From 42aa454218d76ef3cd53ce28f722cc128a159407 Mon Sep 17 00:00:00 2001 From: huangmin <2927933426@qq.com> Date: Thu, 4 Jun 2026 12:41:13 +0800 Subject: [PATCH] =?UTF-8?q?refactor(theme&project):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E9=80=82=E9=85=8D=E4=B8=8E=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增路由感知的Antd主题切换,区分Canvas页面和普通页面的主题配置 2. 重构项目列表状态管理,拆分出全局项目列表hook并支持滚动加载 3. 优化项目操作弹窗样式与交互,新增去重逻辑与多语言适配 4. 重构项目页面切换逻辑,调整首页项目展示结构 --- src/components/AntdThemeProvider.tsx | 122 ++++-- src/components/HomeProjectsContent.tsx | 522 +++++++++++++++++++++---- 2 files changed, 532 insertions(+), 112 deletions(-) diff --git a/src/components/AntdThemeProvider.tsx b/src/components/AntdThemeProvider.tsx index 6c2f7795..3d45e215 100644 --- a/src/components/AntdThemeProvider.tsx +++ b/src/components/AntdThemeProvider.tsx @@ -1,5 +1,6 @@ "use client"; +import { usePathname } from "next/navigation"; import { App as AntdApp, ConfigProvider, theme, type ThemeConfig } from "antd"; import { useEffect, useMemo, useState, type ReactNode } from "react"; @@ -58,6 +59,8 @@ function areTokensEqual(left: CanvasThemeTokens, right: CanvasThemeTokens): bool } export function AntdThemeProvider({ children }: { children: ReactNode }) { + const pathname = usePathname(); + const isCanvasRoute = pathname?.startsWith("/canvas") ?? false; const [canvasTokens, setCanvasTokens] = useState(FALLBACK_TOKENS); useEffect(() => { @@ -79,47 +82,90 @@ export function AntdThemeProvider({ children }: { children: ReactNode }) { return () => observer.disconnect(); }, []); - const antdTheme = useMemo(() => ({ - algorithm: theme.darkAlgorithm, - token: { - colorPrimary: canvasTokens.accent, - colorInfo: canvasTokens.accent, - colorBgBase: canvasTokens.background, - colorBgLayout: canvasTokens.background, - colorBgContainer: canvasTokens.panel, - colorBgElevated: canvasTokens.panelElevated, - colorBgMask: "rgba(0, 0, 0, 0.72)", - colorBorder: canvasTokens.border, - colorBorderSecondary: canvasTokens.border, - colorText: canvasTokens.text, - colorTextBase: canvasTokens.text, - colorTextHeading: canvasTokens.text, - colorTextSecondary: canvasTokens.textSecondary, - colorIcon: canvasTokens.textSecondary, - colorIconHover: canvasTokens.text, - borderRadius: 8, - borderRadiusLG: 8, - wireframe: false, - }, - components: { - Button: { + const antdTheme = useMemo(() => { + if (!isCanvasRoute) { + return { + algorithm: theme.defaultAlgorithm, + token: { + colorPrimary: "#5c3bc7", + colorInfo: "#5c3bc7", + colorBgBase: "#f9f9f9", + colorBgLayout: "#f9f9f9", + colorBgContainer: "#ffffff", + colorBgElevated: "#ffffff", + colorBgMask: "rgba(0, 0, 0, 0.45)", + colorBorder: "#e6e6e6", + colorBorderSecondary: "#f0f0f0", + colorText: "#333333", + colorTextBase: "#333333", + colorTextHeading: "#333333", + colorTextSecondary: "#666666", + colorIcon: "#999999", + colorIconHover: "#666666", + borderRadius: 8, + borderRadiusLG: 8, + wireframe: false, + }, + components: { + Button: { + colorPrimary: "#5c3bc7", + colorPrimaryHover: "#6b4bd4", + colorPrimaryActive: "#4d2fb3", + }, + Modal: { + contentBg: "#ffffff", + headerBg: "#ffffff", + titleColor: "#333333", + }, + Tooltip: { + colorBgSpotlight: "#333333", + }, + }, + }; + } + + return { + algorithm: theme.darkAlgorithm, + token: { colorPrimary: canvasTokens.accent, - colorPrimaryHover: "#60a5fa", - colorPrimaryActive: "#2563eb", + colorInfo: canvasTokens.accent, + colorBgBase: canvasTokens.background, + colorBgLayout: canvasTokens.background, + colorBgContainer: canvasTokens.panel, + colorBgElevated: canvasTokens.panelElevated, + colorBgMask: "rgba(0, 0, 0, 0.72)", + colorBorder: canvasTokens.border, + colorBorderSecondary: canvasTokens.border, + colorText: canvasTokens.text, + colorTextBase: canvasTokens.text, + colorTextHeading: canvasTokens.text, + colorTextSecondary: canvasTokens.textSecondary, + colorIcon: canvasTokens.textSecondary, + colorIconHover: canvasTokens.text, + borderRadius: 8, + borderRadiusLG: 8, + wireframe: false, }, - Modal: { - contentBg: canvasTokens.panel, - headerBg: canvasTokens.panel, - titleColor: canvasTokens.text, + components: { + Button: { + colorPrimary: canvasTokens.accent, + colorPrimaryHover: "#60a5fa", + colorPrimaryActive: "#2563eb", + }, + Modal: { + contentBg: canvasTokens.panel, + headerBg: canvasTokens.panel, + titleColor: canvasTokens.text, + }, + Select: { + optionSelectedBg: "#404040", + }, + Tooltip: { + colorBgSpotlight: canvasTokens.panelElevated, + }, }, - Select: { - optionSelectedBg: "#404040", - }, - Tooltip: { - colorBgSpotlight: canvasTokens.panelElevated, - }, - }, - }), [canvasTokens]); + }; + }, [canvasTokens, isCanvasRoute]); useEffect(() => { ConfigProvider.config({ diff --git a/src/components/HomeProjectsContent.tsx b/src/components/HomeProjectsContent.tsx index f362272a..833e65ea 100644 --- a/src/components/HomeProjectsContent.tsx +++ b/src/components/HomeProjectsContent.tsx @@ -1,13 +1,20 @@ "use client"; import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; +import { + useCallback, + useEffect, + useRef, + useState, + type ReactNode, +} from "react"; import { ProjectActionsDropdown, type ProjectActionKey, } from "@/components/ProjectActionsDropdown"; import { ArrowIcon, + CloseIcon, LeftOutlinedIcon, SearchIcon, StartCreation, @@ -83,6 +90,8 @@ const DEFAULT_PROJECT_PAGE_INFO: CanvasWorkflowPageInfo = { total: 0, }; +const ALL_PROJECTS_LOAD_MORE_OFFSET = 260; + function formatProjectTime(value?: string): string { if (!value) return "-"; const date = new Date(value); @@ -216,7 +225,10 @@ function ProjectSectionGrid({ const router = useRouter(); const canGoPrev = !!pageInfo && pageInfo.page > 1; const canGoNext = !!pageInfo && pageInfo.page < pageInfo.pageCount; - const [firstProject, ...remainingProjects] = projects; + const uniqueProjects = projects.filter( + (project, index, list) => list.findIndex((item) => item.id === project.id) === index + ); + const [firstProject, ...remainingProjects] = uniqueProjects; return (
@@ -307,46 +319,74 @@ function ProjectActionModals({ onCancelDelete: () => void; onConfirmDelete: () => void; }) { + const modalClassName = + "[&_.ant-modal-content]:!rounded-[9px] [&_.ant-modal-content]:!p-[30px] [&_.ant-modal-header]:!mb-0 [&_.ant-modal-body]:!pt-2 [&_.ant-modal-footer]:!mt-6 [&_.ant-modal-footer]:!flex [&_.ant-modal-footer]:!justify-end [&_.ant-modal-footer]:!gap-2.5 [&_.ant-modal-footer_.ant-btn+_.ant-btn]:!ml-0 [&_.ant-modal-close]:!right-2.5 [&_.ant-modal-close]:!top-2.5 [&_.ant-modal-close]:!h-5 [&_.ant-modal-close]:!w-5 [&_.ant-modal-close]:!text-[#999] hover:[&_.ant-modal-close]:!text-[#666]"; + const modalTitleClassName = "text-[24px] font-medium leading-[22px] text-[#333]"; + const cancelButtonClassName = + "!h-[37px] !w-[109px] !rounded-[24px] !border-0 !bg-[#f0f0f0] !p-0 !text-[16px] !font-medium !text-[#666] !shadow-none hover:!bg-[#e6e6e6] hover:!text-[#666]"; + const confirmButtonClassName = + "!h-[37px] !w-[109px] !rounded-[24px] !border-0 !bg-[#5c3bc7] !p-0 !text-[16px] !font-medium !text-white !shadow-none hover:!bg-[#6b4bd4]"; + const deleteConfirmButtonClassName = + "!h-[37px] !w-[109px] !rounded-[24px] !border-0 !bg-[#b7301e] !p-0 !text-[16px] !font-medium !text-white !shadow-none hover:!bg-[#a62b1b]"; + return ( <> 重命名项目} open={renameOpen} + closeIcon={} centered + width={491} + className={modalClassName} confirmLoading={isActionLoading} okText="确认" - cancelText="取消" - okButtonProps={{ disabled: !renameTitle.trim() }} + cancelText="取消(esc)" + okButtonProps={{ + className: confirmButtonClassName, + disabled: !renameTitle.trim(), + }} + cancelButtonProps={{ className: cancelButtonClassName }} onOk={onConfirmRename} onCancel={onCancelRename} destroyOnHidden > -
- + + 项目名称} + className="!mb-0" + > onRenameTitleChange(event.target.value)} placeholder="请输入项目名称" onPressEnter={onConfirmRename} autoFocus + className="!h-[37px] !rounded-[9px] !border-[#e6e6e6] !text-[14px] !shadow-none hover:!border-[#d6ceef] focus:!border-[#5c3bc7]" />
确认删除} open={deleteTitle !== null} + closeIcon={} centered + width={491} + className={modalClassName} confirmLoading={isActionLoading} - okText="确认删除" - okButtonProps={{ danger: true }} - cancelText="取消" + okText="确认" + okButtonProps={{ + danger: true, + className: deleteConfirmButtonClassName, + }} + cancelText="取消(esc)" + cancelButtonProps={{ className: cancelButtonClassName }} onOk={onConfirmDelete} onCancel={onCancelDelete} destroyOnHidden > -

- 确认删除"{deleteTitle ?? ""}"?此操作不可撤销。 +

+ 删除的内容将无法找回

@@ -428,18 +468,21 @@ function useProjectSectionState(pageSize: number, initialPage = 1) { window.setTimeout(() => setProjectCoverProgress(id, undefined), 450); }, [setProjectCoverProgress, stopCoverProgressTimer]); - const loadProjects = useCallback(async (nextPage = page) => { + const loadProjects = useCallback(async (nextPage: number, options?: { append?: boolean }) => { setIsLoadingProjects(true); try { const data = await listCanvasWorkflows({ page: nextPage, pageSize }); - setProjects( - data.list + const nextProjects = data.list .filter((item) => !item.deleted) - .map((item) => mapWorkflowToProject(item)) + .map((item) => mapWorkflowToProject(item)); + setProjects((currentProjects) => + options?.append ? [...currentProjects, ...nextProjects] : nextProjects ); setPageInfo(data.pageInfo); } catch { - setProjects([]); + if (!options?.append) { + setProjects([]); + } setPageInfo({ ...DEFAULT_PROJECT_PAGE_INFO, page: nextPage, @@ -448,44 +491,325 @@ function useProjectSectionState(pageSize: number, initialPage = 1) { } finally { setIsLoadingProjects(false); } - }, [page, pageSize]); + }, [pageSize]); useEffect(() => { - let canceled = false; + void loadProjects(page); + return undefined; + }, [loadProjects, page]); - async function loadPageProjects() { - setIsLoadingProjects(true); - try { - const data = await listCanvasWorkflows({ page, pageSize }); - if (canceled) return; - setProjects( - data.list - .filter((item) => !item.deleted) - .map((item) => mapWorkflowToProject(item)) - ); - setPageInfo(data.pageInfo); - } catch { - if (!canceled) { - setProjects([]); - setPageInfo({ - ...DEFAULT_PROJECT_PAGE_INFO, - page, - pageSize, - }); - } - } finally { - if (!canceled) { - setIsLoadingProjects(false); - } - } + useEffect(() => { + return () => { + stopCoverProgressTimer(); + }; + }, [stopCoverProgressTimer]); + + const handleStartCreation = async () => { + if (isCreating) { + message.warning("项目创建中,请稍后再试"); + return; } - void loadPageProjects(); + setIsCreating(true); + const title = "未命名项目"; + try { + const result = await createCanvasWorkflow({ + title, + desp: "", + properties: buildEmptyWorkflowProperties(title), + }); + const workflowId = (result.data as any)?.id; + router.push(workflowId ? `/canvas?workflowId=${workflowId}` : "/canvas"); + message.success("项目创建成功"); + } catch (error) { + message.error(error instanceof Error ? error.message : "Failed to create workflow."); + setIsCreating(false); + } + }; - return () => { - canceled = true; + const buildFullUpdatePayload = async (id: number) => { + const detail = await getCanvasWorkflowDetail(id); + return { + id, + title: detail.title ?? "", + desp: detail.desp ?? "", + properties: detail.properties ?? "", + coverUrl: detail.coverUrl ?? "", }; - }, [page, pageSize]); + }; + + const handleProjectAction = async (id: number, action: ProjectActionKey) => { + if (actionProjectId !== null) return; + + setActionProjectId(id); + try { + if (action === "rename") { + const payload = await buildFullUpdatePayload(id); + setRenamePayload(payload); + setRenameTitle(payload.title); + return; + } + + if (action === "change-cover") { + coverProjectIdRef.current = id; + setCoverProjectId(id); + coverInputRef.current?.click(); + return; + } + + if (action === "duplicate") { + const detail = await getCanvasWorkflowDetail(id); + const title = detail.title ? `${detail.title} 副本` : "未命名项目副本"; + const result = await createCanvasWorkflow({ + title, + desp: detail.desp ?? "", + properties: detail.properties ?? "", + coverUrl: detail.coverUrl ?? "", + }); + message.success("副本已创建"); + await loadProjects(page); + + // const workflowId = (result.data as any)?.id; + // if (workflowId) { + // router.push(`/canvas?workflowId=${workflowId}`); + // } + return; + } + + if (action === "delete") { + const detail = await getCanvasWorkflowDetail(id); + setDeletePayload({ + id, + title: detail.title ?? detail.code ?? `#${id}`, + }); + } + } catch (error) { + message.error(error instanceof Error ? error.message : "Project action failed."); + } finally { + setActionProjectId(null); + } + }; + + const closeRenameModal = () => { + setRenamePayload(null); + setRenameTitle(""); + }; + + const confirmRenameProject = async () => { + if (!renamePayload || actionProjectId !== null) return; + + setActionProjectId(renamePayload.id); + try { + await updateCanvasWorkflow({ + ...renamePayload, + title: renameTitle, + }); + message.success("项目已重命名"); + setProjects((currentProjects) => + currentProjects.map((project) => + project.id === renamePayload.id + ? { ...project, title: renameTitle.trim() || project.title } + : project + ) + ); + closeRenameModal(); + } catch (error) { + message.error(error instanceof Error ? error.message : "Project rename failed."); + } finally { + setActionProjectId(null); + } + }; + + const closeDeleteModal = () => { + setDeletePayload(null); + }; + + const closeCoverModal = () => { + coverProjectIdRef.current = null; + setCoverProjectId(null); + }; + + const handleCoverFileChange = async (file: File | undefined) => { + const selectedCoverProjectId = coverProjectIdRef.current ?? coverProjectId; + if (!file || selectedCoverProjectId === null || actionProjectId !== null) return; + if (!file.type.startsWith("image/")) { + message.error("请选择图片文件"); + closeCoverModal(); + return; + } + + setActionProjectId(selectedCoverProjectId); + startFakeCoverProgress(selectedCoverProjectId); + try { + const coverPayload = await buildFullUpdatePayload(selectedCoverProjectId); + const uploadedCoverUrl = await uploadCanvasWorkflowCover(file); + await updateCanvasWorkflow({ + ...coverPayload, + coverUrl: uploadedCoverUrl, + }); + message.success("封面已更新"); + finishCoverProgress(selectedCoverProjectId, uploadedCoverUrl); + closeCoverModal(); + } catch (error) { + stopCoverProgressTimer(); + setProjectCoverProgress(selectedCoverProjectId, undefined); + message.error(error instanceof Error ? error.message : "Project cover update failed."); + } finally { + setActionProjectId(null); + if (coverInputRef.current) { + coverInputRef.current.value = ""; + } + } + }; + + const confirmDeleteProject = async () => { + if (!deletePayload || actionProjectId !== null) return; + + setActionProjectId(deletePayload.id); + try { + await deleteCanvasWorkflow(deletePayload.id); + message.success("项目已删除"); + closeDeleteModal(); + await loadProjects(page); + } catch (error) { + message.error(error instanceof Error ? error.message : "Project delete failed."); + } finally { + setActionProjectId(null); + } + }; + + return { + projects, + pageInfo, + isLoadingProjects, + isCreating, + isActionLoading: actionProjectId !== null, + renamePayload, + renameTitle, + setRenameTitle, + closeRenameModal, + confirmRenameProject, + coverInputRef, + closeCoverModal, + handleCoverFileChange, + deletePayload, + closeDeleteModal, + confirmDeleteProject, + handleStartCreation, + handleProjectAction, + goPrevPage: () => setPage((current) => Math.max(1, current - 1)), + goNextPage: () => setPage((current) => Math.min(pageInfo.pageCount, current + 1)), + }; +} + +function useAllProjectsState() { + const pageSize = 20; + const router = useRouter(); + const coverInputRef = useRef(null); + const coverProjectIdRef = useRef(null); + const coverProgressTimerRef = useRef(null); + const loadingMorePageRef = useRef(null); + const [projects, setProjects] = useState([]); + const [page, setPage] = useState(1); + const [pageInfo, setPageInfo] = useState({ + ...DEFAULT_PROJECT_PAGE_INFO, + page: 1, + pageSize, + }); + const [isLoadingProjects, setIsLoadingProjects] = useState(false); + const [isCreating, setIsCreating] = useState(false); + const [actionProjectId, setActionProjectId] = useState(null); + const [renamePayload, setRenamePayload] = useState<{ + id: number; + title: string; + desp: string; + properties: string; + coverUrl: string; + } | null>(null); + const [renameTitle, setRenameTitle] = useState(""); + const [coverProjectId, setCoverProjectId] = useState(null); + const [deletePayload, setDeletePayload] = useState<{ + id: number; + title: string; + } | null>(null); + + const stopCoverProgressTimer = useCallback(() => { + if (coverProgressTimerRef.current) { + window.clearInterval(coverProgressTimerRef.current); + coverProgressTimerRef.current = null; + } + }, []); + + const setProjectCoverProgress = useCallback((id: number, progress?: number) => { + setProjects((currentProjects) => + currentProjects.map((project) => + project.id === id ? { ...project, uploadProgress: progress } : project + ) + ); + }, []); + + const startFakeCoverProgress = useCallback((id: number) => { + stopCoverProgressTimer(); + setProjectCoverProgress(id, 1); + coverProgressTimerRef.current = window.setInterval(() => { + setProjects((currentProjects) => + currentProjects.map((project) => { + if (project.id !== id || typeof project.uploadProgress !== "number") return project; + const nextProgress = Math.min( + 90, + project.uploadProgress + Math.max(1, Math.round((90 - project.uploadProgress) * 0.18)) + ); + return { ...project, uploadProgress: nextProgress }; + }) + ); + }, 300); + }, [setProjectCoverProgress, stopCoverProgressTimer]); + + const finishCoverProgress = useCallback((id: number, coverUrl: string) => { + stopCoverProgressTimer(); + setProjects((currentProjects) => + currentProjects.map((project) => + project.id === id ? { ...project, image: coverUrl, uploadProgress: 100 } : project + ) + ); + window.setTimeout(() => setProjectCoverProgress(id, undefined), 450); + }, [setProjectCoverProgress, stopCoverProgressTimer]); + + const loadProjectsPage = useCallback(async ( + nextPage: number, + options?: { append?: boolean } + ) => { + setIsLoadingProjects(true); + try { + const data = await listCanvasWorkflows({ page: nextPage, pageSize }); + const nextProjects = data.list + .filter((item) => !item.deleted) + .map((item) => mapWorkflowToProject(item)); + setProjects((currentProjects) => { + if (!options?.append) return nextProjects; + const existingIds = new Set(currentProjects.map((project) => project.id)); + const uniqueNextProjects = nextProjects.filter((project) => !existingIds.has(project.id)); + return [...currentProjects, ...uniqueNextProjects]; + }); + setPageInfo(data.pageInfo); + setPage(nextPage); + } catch { + if (!options?.append) { + setProjects([]); + } + setPageInfo({ + ...DEFAULT_PROJECT_PAGE_INFO, + page: nextPage, + pageSize, + }); + } finally { + setIsLoadingProjects(false); + } + }, []); + + useEffect(() => { + void loadProjectsPage(1); + }, [loadProjectsPage]); useEffect(() => { return () => { @@ -549,19 +873,14 @@ function useProjectSectionState(pageSize: number, initialPage = 1) { if (action === "duplicate") { const detail = await getCanvasWorkflowDetail(id); const title = detail.title ? `${detail.title} 副本` : "未命名项目副本"; - const result = await createCanvasWorkflow({ + await createCanvasWorkflow({ title, desp: detail.desp ?? "", properties: detail.properties ?? "", coverUrl: detail.coverUrl ?? "", }); message.success("副本已创建"); - await loadProjects(); - - const workflowId = (result.data as any)?.id; - if (workflowId) { - router.push(`/canvas?workflowId=${workflowId}`); - } + await loadProjectsPage(1); return; } @@ -659,7 +978,7 @@ function useProjectSectionState(pageSize: number, initialPage = 1) { await deleteCanvasWorkflow(deletePayload.id); message.success("项目已删除"); closeDeleteModal(); - await loadProjects(); + await loadProjectsPage(1); } catch (error) { message.error(error instanceof Error ? error.message : "Project delete failed."); } finally { @@ -667,6 +986,16 @@ function useProjectSectionState(pageSize: number, initialPage = 1) { } }; + const loadNextPage = useCallback(() => { + const nextPage = page + 1; + if (isLoadingProjects || nextPage > pageInfo.pageCount) return; + if (loadingMorePageRef.current === nextPage) return; + loadingMorePageRef.current = nextPage; + void loadProjectsPage(nextPage, { append: true }).finally(() => { + loadingMorePageRef.current = null; + }); + }, [isLoadingProjects, loadProjectsPage, page, pageInfo.pageCount]); + return { projects, pageInfo, @@ -686,8 +1015,7 @@ function useProjectSectionState(pageSize: number, initialPage = 1) { confirmDeleteProject, handleStartCreation, handleProjectAction, - goPrevPage: () => setPage((current) => Math.max(1, current - 1)), - goNextPage: () => setPage((current) => Math.min(pageInfo.pageCount, current + 1)), + loadNextPage, }; } @@ -779,8 +1107,10 @@ function RecentProjectsSection({ } function AllProjectsSection({ onBackClick }: { onBackClick: () => void }) { + const [stickyTop, setStickyTop] = useState(90); const { projects, + pageInfo, isLoadingProjects, isCreating, isActionLoading, @@ -797,11 +1127,46 @@ function AllProjectsSection({ onBackClick }: { onBackClick: () => void }) { confirmDeleteProject, handleStartCreation, handleProjectAction, - } = useProjectSectionState(20); + loadNextPage, + } = useAllProjectsState(); + const canLoadMoreProjects = pageInfo.page < pageInfo.pageCount; + + useEffect(() => { + const updateStickyTop = () => { + const header = document.querySelector("header"); + setStickyTop(header?.getBoundingClientRect().height ?? 0); + }; + + updateStickyTop(); + window.addEventListener("resize", updateStickyTop); + return () => { + window.removeEventListener("resize", updateStickyTop); + }; + }, []); + + useEffect(() => { + const handleScroll = () => { + if (!canLoadMoreProjects || isLoadingProjects) return; + const distanceToBottom = + document.documentElement.scrollHeight - window.innerHeight - window.scrollY; + if (distanceToBottom <= ALL_PROJECTS_LOAD_MORE_OFFSET) { + loadNextPage(); + } + }; + + window.addEventListener("scroll", handleScroll, { passive: true }); + handleScroll(); + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, [canLoadMoreProjects, isLoadingProjects, loadNextPage]); return (
-
+

- 最近项目 + 全部项目

@@ -823,6 +1188,15 @@ function AllProjectsSection({ onBackClick }: { onBackClick: () => void }) { onStartCreation={handleStartCreation} onProjectAction={handleProjectAction} /> +
+ {isLoadingProjects && projects.length > 0 + ? "加载中..." + : canLoadMoreProjects + ? "继续下拉加载更多" + : projects.length > 0 + ? "没有更多项目" + : null} +
-
- {heroSection} - - setIsShowAll(true)} - /> + {isShowAll ? ( + setIsShowAll(false)} /> + ) : ( + <> + {heroSection} - -
+ setIsShowAll(true)} + /> -
- setIsShowAll(false)} /> -
+ + + )}
); }