|
|
@ -1,7 +1,7 @@ |
|
|
"use client"; |
|
|
"use client"; |
|
|
|
|
|
|
|
|
import Link from "next/link"; |
|
|
import { useRouter } from "next/navigation"; |
|
|
import { useEffect, useState, type ReactNode } from "react"; |
|
|
import { useCallback, useEffect, useState, type ReactNode } from "react"; |
|
|
import { ProjectActionsDropdown } from "@/components/ProjectActionsDropdown"; |
|
|
import { ProjectActionsDropdown } from "@/components/ProjectActionsDropdown"; |
|
|
import { |
|
|
import { |
|
|
ArrowIcon, |
|
|
ArrowIcon, |
|
|
@ -10,8 +10,14 @@ import { |
|
|
StartCreation, |
|
|
StartCreation, |
|
|
} from "@/components/icons"; |
|
|
} from "@/components/icons"; |
|
|
import { authFetch } from "@/utils/authFetch"; |
|
|
import { authFetch } from "@/utils/authFetch"; |
|
|
|
|
|
import { |
|
|
|
|
|
createCanvasWorkflow, |
|
|
|
|
|
listCanvasWorkflows, |
|
|
|
|
|
type CanvasWorkflowListItem, |
|
|
|
|
|
} from "@/lib/canvasWorkflowApi"; |
|
|
|
|
|
import { message } from 'antd'; |
|
|
type ProjectData = { |
|
|
type ProjectData = { |
|
|
|
|
|
id: number; |
|
|
title: string; |
|
|
title: string; |
|
|
date: string; |
|
|
date: string; |
|
|
image: string; |
|
|
image: string; |
|
|
@ -23,6 +29,7 @@ type ShowData = { |
|
|
image: string; |
|
|
image: string; |
|
|
avatar: string; |
|
|
avatar: string; |
|
|
vip?: boolean; |
|
|
vip?: boolean; |
|
|
|
|
|
data: string; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
type TemplateCategoryData = { |
|
|
type TemplateCategoryData = { |
|
|
@ -42,6 +49,7 @@ type CanvasTemplateItem = { |
|
|
categoryId: number; |
|
|
categoryId: number; |
|
|
type: number; |
|
|
type: number; |
|
|
coverUrl: string; |
|
|
coverUrl: string; |
|
|
|
|
|
properties: string; |
|
|
name: string; |
|
|
name: string; |
|
|
desp: string; |
|
|
desp: string; |
|
|
deleted: boolean; |
|
|
deleted: boolean; |
|
|
@ -59,17 +67,53 @@ type HomeProjectsContentProps = { |
|
|
heroSection: ReactNode; |
|
|
heroSection: ReactNode; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
function ProjectCard({ title, date, image }: ProjectData) { |
|
|
function formatProjectTime(value?: string): string { |
|
|
|
|
|
if (!value) return "-"; |
|
|
|
|
|
const date = new Date(value); |
|
|
|
|
|
if (Number.isNaN(date.getTime())) return value; |
|
|
|
|
|
return date.toLocaleDateString(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function mapWorkflowToProject(item: CanvasWorkflowListItem): ProjectData { |
|
|
|
|
|
return { |
|
|
|
|
|
id: item.id, |
|
|
|
|
|
title: item.title?.trim() || item.code?.trim() || `#${item.id}`, |
|
|
|
|
|
date: formatProjectTime(item.updateTime || item.createTime), |
|
|
|
|
|
image: "", |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function buildEmptyWorkflowProperties(title: string): string { |
|
|
|
|
|
return JSON.stringify({ |
|
|
|
|
|
name: title, |
|
|
|
|
|
nodes: [], |
|
|
|
|
|
edges: [], |
|
|
|
|
|
edgeStyle: "curved", |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function ProjectCard({ |
|
|
|
|
|
title, |
|
|
|
|
|
date, |
|
|
|
|
|
image, |
|
|
|
|
|
onOpen, |
|
|
|
|
|
}: ProjectData & { onOpen: () => void }) { |
|
|
return ( |
|
|
return ( |
|
|
<article className="min-w-0"> |
|
|
<article className="min-w-0"> |
|
|
<div className="h-[139px] overflow-hidden rounded-[24px] bg-[#ddd]"> |
|
|
<button |
|
|
<img src={image} alt="" className="h-full w-full object-cover" /> |
|
|
type="button" |
|
|
</div> |
|
|
onClick={onOpen} |
|
|
|
|
|
className="block h-[139px] w-full overflow-hidden rounded-[24px] bg-[#ddd] text-left" |
|
|
|
|
|
> |
|
|
|
|
|
<img src={image || '/empty_project.png'} alt="" className="h-full w-full object-cover" /> |
|
|
|
|
|
</button> |
|
|
<div className="mt-3 flex items-start justify-between gap-3"> |
|
|
<div className="mt-3 flex items-start justify-between gap-3"> |
|
|
<div className="min-w-0"> |
|
|
<div className="min-w-0"> |
|
|
<h3 className="truncate text-[16px] font-medium leading-tight text-[#333]"> |
|
|
<button type="button" onClick={onOpen} className="block max-w-full text-left"> |
|
|
{title} |
|
|
<h3 className="truncate text-[16px] font-medium leading-tight text-[#333]"> |
|
|
</h3> |
|
|
{title} |
|
|
|
|
|
</h3> |
|
|
|
|
|
</button> |
|
|
<p className="mt-1 text-[13px] leading-tight text-[#666]">{date}</p> |
|
|
<p className="mt-1 text-[13px] leading-tight text-[#666]">{date}</p> |
|
|
</div> |
|
|
</div> |
|
|
<ProjectActionsDropdown title={title} label={"更多操作"} /> |
|
|
<ProjectActionsDropdown title={title} label={"更多操作"} /> |
|
|
@ -78,11 +122,11 @@ function ProjectCard({ title, date, image }: ProjectData) { |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function ShowCard({ title, subtitle, image, avatar, vip }: ShowData) { |
|
|
function ShowCard({ title, subtitle, image, avatar, vip, data, onOpen }: ShowData & { onOpen: (data: ShowData) => void }) { |
|
|
return ( |
|
|
return ( |
|
|
<article className="min-w-0"> |
|
|
<article className="min-w-0" onClick={() => onOpen({ title, subtitle, image, avatar, vip, data })}> |
|
|
<div className="h-[189px] overflow-hidden rounded-[24px] bg-[#ddd]"> |
|
|
<div className="h-[189px] overflow-hidden rounded-[24px] bg-[#ddd]"> |
|
|
<img src={image} alt="" className="h-full w-full object-cover" /> |
|
|
<img src={image || '/empty_project.png'} alt="" className="h-full w-full object-cover" /> |
|
|
</div> |
|
|
</div> |
|
|
<div className="mt-3"> |
|
|
<div className="mt-3"> |
|
|
<div className="flex items-center gap-[5px]"> |
|
|
<div className="flex items-center gap-[5px]"> |
|
|
@ -115,7 +159,67 @@ function ProjectsSection({ |
|
|
onBackClick?: () => void; |
|
|
onBackClick?: () => void; |
|
|
onAllProjectsClick?: () => void; |
|
|
onAllProjectsClick?: () => void; |
|
|
}) { |
|
|
}) { |
|
|
|
|
|
const router = useRouter(); |
|
|
|
|
|
|
|
|
const [projects, setProjects] = useState<ProjectData[]>([]); |
|
|
const [projects, setProjects] = useState<ProjectData[]>([]); |
|
|
|
|
|
const [isLoadingProjects, setIsLoadingProjects] = useState(false); |
|
|
|
|
|
const [isCreating, setIsCreating] = useState(false); |
|
|
|
|
|
const pageSize = onAllProjectsClick ? 4 : 20; |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
let canceled = false; |
|
|
|
|
|
|
|
|
|
|
|
async function loadProjects() { |
|
|
|
|
|
setIsLoadingProjects(true); |
|
|
|
|
|
try { |
|
|
|
|
|
const data = await listCanvasWorkflows({ page: 1, pageSize }); |
|
|
|
|
|
if (canceled) return; |
|
|
|
|
|
setProjects( |
|
|
|
|
|
data.list |
|
|
|
|
|
.filter((item) => !item.deleted) |
|
|
|
|
|
.map((item) => mapWorkflowToProject(item)) |
|
|
|
|
|
); |
|
|
|
|
|
} catch { |
|
|
|
|
|
if (!canceled) { |
|
|
|
|
|
setProjects([]); |
|
|
|
|
|
} |
|
|
|
|
|
} finally { |
|
|
|
|
|
if (!canceled) { |
|
|
|
|
|
setIsLoadingProjects(false); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void loadProjects(); |
|
|
|
|
|
|
|
|
|
|
|
return () => { |
|
|
|
|
|
canceled = true; |
|
|
|
|
|
}; |
|
|
|
|
|
}, [pageSize]); |
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<section className={`${className}`}> |
|
|
<section className={`${className}`}> |
|
|
<div className="mb-5 flex items-center justify-between"> |
|
|
<div className="mb-5 flex items-center justify-between"> |
|
|
@ -145,17 +249,28 @@ function ProjectsSection({ |
|
|
)} |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
<div className="relative grid gap-5 md:grid-cols-2 xl:grid-cols-5"> |
|
|
<div className="relative grid gap-5 md:grid-cols-2 xl:grid-cols-5"> |
|
|
<Link |
|
|
<button |
|
|
href="/canvas" |
|
|
type="button" |
|
|
className="flex h-[139px] flex-col items-center justify-center rounded-[24px] border border-[#d6ceef] bg-[linear-gradient(123deg,#f0f0f0_20%,#d6ceef_100%)] transition hover:border-[#5c3bc7]" |
|
|
onClick={handleStartCreation} |
|
|
|
|
|
disabled={isCreating} |
|
|
|
|
|
className="flex h-[139px] flex-col items-center justify-center rounded-[24px] border border-[#d6ceef] bg-[linear-gradient(123deg,#f0f0f0_20%,#d6ceef_100%)] transition hover:border-[#5c3bc7] disabled:cursor-not-allowed disabled:opacity-70" |
|
|
> |
|
|
> |
|
|
<StartCreation /> |
|
|
<StartCreation /> |
|
|
<span className="mt-2 text-[18px] font-medium leading-none text-[#999]"> |
|
|
<span className="mt-2 text-[18px] font-medium leading-none text-[#999]"> |
|
|
开始创作 |
|
|
开始创作 |
|
|
</span> |
|
|
</span> |
|
|
</Link> |
|
|
</button> |
|
|
|
|
|
{isLoadingProjects && projects.length === 0 ? ( |
|
|
|
|
|
<div className="flex h-[139px] items-center justify-center rounded-[24px] bg-white text-[14px] text-[#999]"> |
|
|
|
|
|
Loading... |
|
|
|
|
|
</div> |
|
|
|
|
|
) : null} |
|
|
{projects.map((project) => ( |
|
|
{projects.map((project) => ( |
|
|
<ProjectCard key={project.title} {...project} /> |
|
|
<ProjectCard |
|
|
|
|
|
key={project.id} |
|
|
|
|
|
{...project} |
|
|
|
|
|
onOpen={() => router.push(`/canvas?workflowId=${project.id}`)} |
|
|
|
|
|
/> |
|
|
))} |
|
|
))} |
|
|
{onAllProjectsClick && projects.length === 4 && ( |
|
|
{onAllProjectsClick && projects.length === 4 && ( |
|
|
<> |
|
|
<> |
|
|
@ -176,13 +291,14 @@ function ProjectsSection({ |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function mapTemplateToShow(item: CanvasTemplateItem, index: number): ShowData { |
|
|
function mapTemplateToShow(item: CanvasTemplateItem): ShowData { |
|
|
return { |
|
|
return { |
|
|
title: item.name || "未命名", |
|
|
title: item.name || "未命名", |
|
|
subtitle: item.desp || item.categoryName || "画布模板", |
|
|
subtitle: item.desp || item.categoryName || "画布模板", |
|
|
image: item.coverUrl || '', |
|
|
image: item.coverUrl || '', |
|
|
avatar: item.categoryName?.slice(0, 1) || item.name?.slice(0, 1) || "画", |
|
|
avatar: item.categoryName?.slice(0, 1) || item.name?.slice(0, 1) || "画", |
|
|
vip: item.type === 1, |
|
|
vip: item.type === 1, |
|
|
|
|
|
data: item.properties || '', |
|
|
}; |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -194,6 +310,9 @@ function ShowsSection() { |
|
|
const [dataList, setDataList] = useState<ShowData[]>([]); |
|
|
const [dataList, setDataList] = useState<ShowData[]>([]); |
|
|
const [searchQuery, setSearchQuery] = useState(""); |
|
|
const [searchQuery, setSearchQuery] = useState(""); |
|
|
const selectedCategoryId = categoryList[activeTab]?.id ?? null; |
|
|
const selectedCategoryId = categoryList[activeTab]?.id ?? null; |
|
|
|
|
|
const [isCreating, setIsCreating] = useState(false); |
|
|
|
|
|
|
|
|
|
|
|
const router = useRouter(); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
let canceled = false; |
|
|
let canceled = false; |
|
|
@ -257,7 +376,7 @@ function ShowsSection() { |
|
|
setDataList( |
|
|
setDataList( |
|
|
list |
|
|
list |
|
|
.filter((item) => !item.deleted) |
|
|
.filter((item) => !item.deleted) |
|
|
.map((item, index) => mapTemplateToShow(item, index)) |
|
|
.map((item) => mapTemplateToShow(item)) |
|
|
); |
|
|
); |
|
|
} catch { |
|
|
} catch { |
|
|
if (!controller.signal.aborted) { |
|
|
if (!controller.signal.aborted) { |
|
|
@ -272,6 +391,24 @@ function ShowsSection() { |
|
|
}; |
|
|
}; |
|
|
}, [searchQuery, selectedCategoryId]); |
|
|
}, [searchQuery, selectedCategoryId]); |
|
|
|
|
|
|
|
|
|
|
|
const onOpen = useCallback(async (data: ShowData) => { |
|
|
|
|
|
if (isCreating) return; |
|
|
|
|
|
setIsCreating(true); |
|
|
|
|
|
const title = data.title || "未命名项目"; |
|
|
|
|
|
try { |
|
|
|
|
|
const result = await createCanvasWorkflow({ |
|
|
|
|
|
title, |
|
|
|
|
|
desp: data.subtitle || "", |
|
|
|
|
|
properties: data.data || buildEmptyWorkflowProperties(title), |
|
|
|
|
|
}); |
|
|
|
|
|
const workflowId = (result.data as any)?.id |
|
|
|
|
|
router.push(workflowId ? `/canvas?workflowId=${workflowId}` : "/canvas"); |
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
window.alert(error instanceof Error ? error.message : "Failed to create workflow."); |
|
|
|
|
|
setIsCreating(false); |
|
|
|
|
|
} |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<section className="mt-[64px]"> |
|
|
<section className="mt-[64px]"> |
|
|
<div className="mb-5 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between"> |
|
|
<div className="mb-5 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between"> |
|
|
@ -308,7 +445,7 @@ function ShowsSection() { |
|
|
</div> |
|
|
</div> |
|
|
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4"> |
|
|
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4"> |
|
|
{dataList.map((show) => ( |
|
|
{dataList.map((show) => ( |
|
|
<ShowCard key={show.title} {...show} /> |
|
|
<ShowCard key={show.title} {...show} onOpen={onOpen} /> |
|
|
))} |
|
|
))} |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
</section> |
|
|
|