@@ -255,160 +268,24 @@ function HeroCard({
);
}
-function ProjectCard({
- title,
- date,
- image,
-}: {
- title: string;
- date: string;
- image: string;
-}) {
- return (
-
-
-
-
-
-
-
- {title}
-
- {date}
-
-
-
-
- );
-}
+export default function Home() {
+ const [heroCards, setHeroCards] = useState([]);
+ useEffect(() => {
+ getHeroCards().then(setHeroCards);
+ }, []);
-function ShowCard({
- title,
- subtitle,
- image,
- avatar,
- vip,
-}: {
- title: string;
- subtitle: string;
- image: string;
- avatar: string;
- vip?: boolean;
-}) {
- return (
-
-
-
-
-
-
-
- {avatar}
-
-
- {title}
-
- {vip ? (
-
- VIP
-
- ) : null}
-
-
- {subtitle}
-
-
-
+ const heroSection = (
+
+ {heroCards.map((card) => (
+
+ ))}
+
);
-}
-export default function Home() {
return (
-
-
-
- {heroCards.map((card) => (
-
- ))}
-
-
-
-
-
- 最近项目
-
-
- 全部项目
-
-
-
-
-
-
- 开始创作
-
-
- {projects.map((project) => (
-
- ))}
-
-
-
-
-
-
-
-
-
- TV show
-
-
- {tabs.map((tab, index) => (
-
- ))}
-
-
-
-
-
-
-
-
- {shows.map((show) => (
-
- ))}
-
-
-
+
);
}
diff --git a/src/components/HomeProjectsContent.tsx b/src/components/HomeProjectsContent.tsx
new file mode 100644
index 00000000..1a9c2588
--- /dev/null
+++ b/src/components/HomeProjectsContent.tsx
@@ -0,0 +1,480 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { useCallback, useEffect, useState, type ReactNode } from "react";
+import { ProjectActionsDropdown } from "@/components/ProjectActionsDropdown";
+import {
+ ArrowIcon,
+ LeftOutlinedIcon,
+ SearchIcon,
+ StartCreation,
+} from "@/components/icons";
+import { authFetch } from "@/utils/authFetch";
+import {
+ createCanvasWorkflow,
+ listCanvasWorkflows,
+ type CanvasWorkflowListItem,
+} from "@/lib/canvasWorkflowApi";
+import { message } from 'antd';
+type ProjectData = {
+ id: number;
+ title: string;
+ date: string;
+ image: string;
+};
+
+type ShowData = {
+ title: string;
+ subtitle: string;
+ image: string;
+ avatar: string;
+ vip?: boolean;
+ data: string;
+};
+
+type TemplateCategoryData = {
+ id: number | null;
+ name: string;
+};
+
+type CanvasTemplateCategoryItem = {
+ id: number;
+ name: string;
+ sort: number;
+ deleted: boolean;
+};
+
+type CanvasTemplateItem = {
+ id: number;
+ categoryId: number;
+ type: number;
+ coverUrl: string;
+ properties: string;
+ name: string;
+ desp: string;
+ deleted: boolean;
+ categoryName: string;
+};
+
+type ListResponse = {
+ data?: {
+ list?: T[];
+ };
+ status?: string;
+};
+
+type HomeProjectsContentProps = {
+ heroSection: ReactNode;
+};
+
+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 (
+
+
+
+
+
+ {date}
+
+
+
+
+ );
+}
+
+function ShowCard({ title, subtitle, image, avatar, vip, data, onOpen }: ShowData & { onOpen: (data: ShowData) => void }) {
+ return (
+ onOpen({ title, subtitle, image, avatar, vip, data })}>
+
+
+
+
+
+
+ {avatar}
+
+
+ {title}
+
+ {vip ? (
+
+ VIP
+
+ ) : null}
+
+
+ {subtitle}
+
+
+
+ );
+}
+
+function ProjectsSection({
+ className = "",
+ onAllProjectsClick,
+ onBackClick,
+}: {
+ className?: string;
+ onBackClick?: () => void;
+ onAllProjectsClick?: () => void;
+}) {
+ const router = useRouter();
+
+ const [projects, setProjects] = useState([]);
+ 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 (
+
+
+
+ {onBackClick && (
+
+
+ 返回
+
+ )}
+
+ 最近项目
+
+
+ {!!onAllProjectsClick && (
+
+ )}
+
+
+
+ {isLoadingProjects && projects.length === 0 ? (
+
+ Loading...
+
+ ) : null}
+ {projects.map((project) => (
+ router.push(`/canvas?workflowId=${project.id}`)}
+ />
+ ))}
+ {onAllProjectsClick && projects.length === 4 && (
+ <>
+
+
+ >
+ )}
+
+
+ );
+}
+
+
+function mapTemplateToShow(item: CanvasTemplateItem): ShowData {
+ return {
+ title: item.name || "未命名",
+ subtitle: item.desp || item.categoryName || "画布模板",
+ image: item.coverUrl || '',
+ avatar: item.categoryName?.slice(0, 1) || item.name?.slice(0, 1) || "画",
+ vip: item.type === 1,
+ data: item.properties || '',
+ };
+}
+
+function ShowsSection() {
+ const [activeTab, setActiveTab] = useState(0);
+ const [categoryList, setCategoryList] = useState([
+ { id: null, name: "全部" },
+ ]);
+ const [dataList, setDataList] = useState([]);
+ const [searchQuery, setSearchQuery] = useState("");
+ const selectedCategoryId = categoryList[activeTab]?.id ?? null;
+ const [isCreating, setIsCreating] = useState(false);
+
+ const router = useRouter();
+
+ useEffect(() => {
+ let canceled = false;
+
+ async function loadCategories() {
+ try {
+ const params = new URLSearchParams({ keyword: "" });
+ const response = await authFetch(`/api/canvas-templates/categories?${params.toString()}`);
+ if (!response.ok) return;
+
+ const result = (await response.json()) as ListResponse;
+ const list = result.data?.list;
+ if (!Array.isArray(list)) return;
+
+ const categories = list
+ .filter((item) => !item.deleted)
+ .sort((a, b) => a.sort - b.sort)
+ .map((item) => ({ id: item.id, name: item.name }))
+ .filter((item) => item.name);
+
+ if (!canceled && categories.length > 0) {
+ setCategoryList([{ id: null, name: "全部" }, ...categories]);
+ }
+ } catch {
+ // Keep fallback category.
+ }
+ }
+
+ loadCategories();
+
+ return () => {
+ canceled = true;
+ };
+ }, []);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ const timeout = window.setTimeout(async () => {
+ try {
+ const params = new URLSearchParams({
+ keyword: searchQuery,
+ categoryId: selectedCategoryId ? String(selectedCategoryId) : "",
+ type: "",
+ userId: "",
+ });
+ const response = await authFetch(`/api/canvas-templates?${params.toString()}`, {
+ signal: controller.signal,
+ });
+ if (!response.ok) {
+ setDataList([]);
+ return;
+ }
+
+ const result = (await response.json()) as ListResponse;
+ const list = result.data?.list;
+ if (!Array.isArray(list)) {
+ setDataList([]);
+ return;
+ }
+
+ setDataList(
+ list
+ .filter((item) => !item.deleted)
+ .map((item) => mapTemplateToShow(item))
+ );
+ } catch {
+ if (!controller.signal.aborted) {
+ setDataList([]);
+ }
+ }
+ }, 250);
+
+ return () => {
+ window.clearTimeout(timeout);
+ controller.abort();
+ };
+ }, [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 (
+
+
+
+
+ TV show
+
+
+ {categoryList.map((tab, index) => (
+
+ ))}
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="请输入搜索的内容"
+ className="min-w-0 flex-1 bg-transparent text-[14px] outline-none placeholder:text-[#999]"
+ />
+
+
+
+
+ {dataList.map((show) => (
+
+ ))}
+
+
+ );
+}
+
+export function HomeProjectsContent({
+ heroSection,
+}: HomeProjectsContentProps) {
+ const [isShowAll, setIsShowAll] = useState(false);
+
+ return (
+
+
+ {heroSection}
+
+ setIsShowAll(true)}
+ />
+
+
+
+
+
+ setIsShowAll(false)}
+ />
+
+
+ );
+}
diff --git a/src/components/icons/index.tsx b/src/components/icons/index.tsx
index 593aea2a..52bbf569 100644
--- a/src/components/icons/index.tsx
+++ b/src/components/icons/index.tsx
@@ -167,3 +167,17 @@ export const StartCreation = () => (
);
+
+export const LeftOutlinedIcon = () => (
+
+);
diff --git a/src/lib/canvasWorkflowApi.ts b/src/lib/canvasWorkflowApi.ts
index 58cf5430..5691a539 100644
--- a/src/lib/canvasWorkflowApi.ts
+++ b/src/lib/canvasWorkflowApi.ts
@@ -23,13 +23,19 @@ export interface CreateCanvasWorkflowResponse {
export interface CanvasWorkflowListItem {
id: number;
+ userId?: number;
+ gatewayUserId?: number;
title?: string;
desp?: string;
properties?: string;
code?: string;
+ apikey?: string;
+ apikeyId?: number;
status?: number;
createTime?: string;
updateTime?: string;
+ deleted?: boolean;
+ deleteTime?: string | null;
}
export interface CanvasWorkflowPageInfo {
diff --git a/src/utils/getToken.ts b/src/utils/getToken.ts
new file mode 100644
index 00000000..44c591b9
--- /dev/null
+++ b/src/utils/getToken.ts
@@ -0,0 +1,12 @@
+import { NextRequest } from "next/server";
+
+export const getToken = (request: NextRequest): string | null => {
+ const tokenHeader = request.headers.get("token");
+ if (tokenHeader?.trim()) {
+ return tokenHeader;
+ }
+
+ const authorization = request.headers.get("authorization");
+ const bearerToken = authorization?.match(/^Bearer\s+(.+)$/i)?.[1];
+ return bearerToken?.trim() || null;
+};
\ No newline at end of file
- {title} -
-{date}
-- {title} -
- {vip ? ( - - VIP - - ) : null} -- {subtitle} -
-- 最近项目 -
- - 全部项目- TV show -
-{date}
++ {title} +
+ {vip ? ( + + VIP + + ) : null} ++ {subtitle} +
+