|
|
|
@ -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 ( |
|
|
|
<div className="relative grid gap-5 md:grid-cols-2 xl:grid-cols-5"> |
|
|
|
@ -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 ( |
|
|
|
<> |
|
|
|
<Modal |
|
|
|
title="重命名项目" |
|
|
|
title={<span className={modalTitleClassName}>重命名项目</span>} |
|
|
|
open={renameOpen} |
|
|
|
closeIcon={<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 |
|
|
|
> |
|
|
|
<Form layout="vertical" preserve={false}> |
|
|
|
<Form.Item label="项目名称"> |
|
|
|
<Form layout="vertical" preserve={false} className="!mt-[7px]"> |
|
|
|
<Form.Item |
|
|
|
label={<span className="text-[14px] leading-[22px] text-[#666]">项目名称</span>} |
|
|
|
className="!mb-0" |
|
|
|
> |
|
|
|
<Input |
|
|
|
value={renameTitle} |
|
|
|
onChange={(event) => 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]" |
|
|
|
/> |
|
|
|
</Form.Item> |
|
|
|
</Form> |
|
|
|
</Modal> |
|
|
|
<Modal |
|
|
|
title="删除项目" |
|
|
|
title={<span className={modalTitleClassName}>确认删除</span>} |
|
|
|
open={deleteTitle !== null} |
|
|
|
closeIcon={<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 |
|
|
|
> |
|
|
|
<p className="m-0 text-sm leading-6"> |
|
|
|
确认删除"{deleteTitle ?? ""}"?此操作不可撤销。 |
|
|
|
<p className="m-0 text-[14px] leading-[22px] text-[#666]"> |
|
|
|
删除的内容将无法找回 |
|
|
|
</p> |
|
|
|
</Modal> |
|
|
|
</> |
|
|
|
@ -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 { |
|
|
|
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]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
return () => { |
|
|
|
stopCoverProgressTimer(); |
|
|
|
}; |
|
|
|
}, [stopCoverProgressTimer]); |
|
|
|
|
|
|
|
const handleStartCreation = async () => { |
|
|
|
if (isCreating) { |
|
|
|
message.warning("项目创建中,请稍后再试"); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
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); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const buildFullUpdatePayload = async (id: number) => { |
|
|
|
const detail = await getCanvasWorkflowDetail(id); |
|
|
|
return { |
|
|
|
id, |
|
|
|
title: detail.title ?? "", |
|
|
|
desp: detail.desp ?? "", |
|
|
|
properties: detail.properties ?? "", |
|
|
|
coverUrl: detail.coverUrl ?? "", |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
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)), |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
async function loadPageProjects() { |
|
|
|
function useAllProjectsState() { |
|
|
|
const pageSize = 20; |
|
|
|
const router = useRouter(); |
|
|
|
const coverInputRef = useRef<HTMLInputElement>(null); |
|
|
|
const coverProjectIdRef = useRef<number | null>(null); |
|
|
|
const coverProgressTimerRef = useRef<number | null>(null); |
|
|
|
const loadingMorePageRef = useRef<number | null>(null); |
|
|
|
const [projects, setProjects] = useState<ProjectData[]>([]); |
|
|
|
const [page, setPage] = useState(1); |
|
|
|
const [pageInfo, setPageInfo] = useState<CanvasWorkflowPageInfo>({ |
|
|
|
...DEFAULT_PROJECT_PAGE_INFO, |
|
|
|
page: 1, |
|
|
|
pageSize, |
|
|
|
}); |
|
|
|
const [isLoadingProjects, setIsLoadingProjects] = useState(false); |
|
|
|
const [isCreating, setIsCreating] = useState(false); |
|
|
|
const [actionProjectId, setActionProjectId] = useState<number | null>(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<number | null>(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, pageSize }); |
|
|
|
if (canceled) return; |
|
|
|
setProjects( |
|
|
|
data.list |
|
|
|
const data = await listCanvasWorkflows({ page: nextPage, pageSize }); |
|
|
|
const nextProjects = data.list |
|
|
|
.filter((item) => !item.deleted) |
|
|
|
.map((item) => mapWorkflowToProject(item)) |
|
|
|
); |
|
|
|
.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 (!canceled) { |
|
|
|
if (!options?.append) { |
|
|
|
setProjects([]); |
|
|
|
} |
|
|
|
setPageInfo({ |
|
|
|
...DEFAULT_PROJECT_PAGE_INFO, |
|
|
|
page, |
|
|
|
page: nextPage, |
|
|
|
pageSize, |
|
|
|
}); |
|
|
|
} |
|
|
|
} finally { |
|
|
|
if (!canceled) { |
|
|
|
setIsLoadingProjects(false); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
void loadPageProjects(); |
|
|
|
}, []); |
|
|
|
|
|
|
|
return () => { |
|
|
|
canceled = true; |
|
|
|
}; |
|
|
|
}, [page, pageSize]); |
|
|
|
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 ( |
|
|
|
<section> |
|
|
|
<div className="mb-5 flex items-center justify-between"> |
|
|
|
<div |
|
|
|
className="sticky z-20 -mx-5 mb-5 flex items-center justify-between border-b border-[#f0f0f0] bg-[#f9f9f9]/95 px-5 py-3 backdrop-blur" |
|
|
|
style={{ top: stickyTop }} |
|
|
|
> |
|
|
|
<div className="flex items-center justify-center gap-x-1 overflow-hidden whitespace-nowrap"> |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
@ -812,7 +1177,7 @@ function AllProjectsSection({ onBackClick }: { onBackClick: () => void }) { |
|
|
|
<span>返回</span> |
|
|
|
</button> |
|
|
|
<h2 className="text-[20px] font-bold leading-none text-[#333]"> |
|
|
|
最近项目 |
|
|
|
全部项目 |
|
|
|
</h2> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -823,6 +1188,15 @@ function AllProjectsSection({ onBackClick }: { onBackClick: () => void }) { |
|
|
|
onStartCreation={handleStartCreation} |
|
|
|
onProjectAction={handleProjectAction} |
|
|
|
/> |
|
|
|
<div className="flex h-14 items-center justify-center text-[13px] font-medium text-[#999]"> |
|
|
|
{isLoadingProjects && projects.length > 0 |
|
|
|
? "加载中..." |
|
|
|
: canLoadMoreProjects |
|
|
|
? "继续下拉加载更多" |
|
|
|
: projects.length > 0 |
|
|
|
? "没有更多项目" |
|
|
|
: null} |
|
|
|
</div> |
|
|
|
<ProjectActionModals |
|
|
|
isActionLoading={isActionLoading} |
|
|
|
renameOpen={!!renamePayload} |
|
|
|
@ -1021,7 +1395,10 @@ export function HomeProjectsContent({ |
|
|
|
|
|
|
|
return ( |
|
|
|
<div className="mx-auto w-full max-w-[1456px] px-5 pb-16 pt-[30px]"> |
|
|
|
<div className={`${isShowAll ? "hidden" : ""}`}> |
|
|
|
{isShowAll ? ( |
|
|
|
<AllProjectsSection onBackClick={() => setIsShowAll(false)} /> |
|
|
|
) : ( |
|
|
|
<> |
|
|
|
{heroSection} |
|
|
|
|
|
|
|
<RecentProjectsSection |
|
|
|
@ -1030,11 +1407,8 @@ export function HomeProjectsContent({ |
|
|
|
/> |
|
|
|
|
|
|
|
<ShowsSection /> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className={`${!isShowAll ? "hidden" : ""}`}> |
|
|
|
<AllProjectsSection onBackClick={() => setIsShowAll(false)} /> |
|
|
|
</div> |
|
|
|
</> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
); |
|
|
|
} |
|
|
|
|