@@ -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..2853b1b2
--- /dev/null
+++ b/src/components/HomeProjectsContent.tsx
@@ -0,0 +1,343 @@
+"use client";
+
+import Link from "next/link";
+import { useEffect, useState, type ReactNode } from "react";
+import { ProjectActionsDropdown } from "@/components/ProjectActionsDropdown";
+import {
+ ArrowIcon,
+ LeftOutlinedIcon,
+ SearchIcon,
+ StartCreation,
+} from "@/components/icons";
+import { authFetch } from "@/utils/authFetch";
+
+type ProjectData = {
+ title: string;
+ date: string;
+ image: string;
+};
+
+type ShowData = {
+ title: string;
+ subtitle: string;
+ image: string;
+ avatar: string;
+ vip?: boolean;
+};
+
+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;
+ name: string;
+ desp: string;
+ deleted: boolean;
+ categoryName: string;
+};
+
+type ListResponse = {
+ data?: {
+ list?: T[];
+ };
+ status?: string;
+};
+
+type HomeProjectsContentProps = {
+ heroSection: ReactNode;
+};
+
+function ProjectCard({ title, date, image }: ProjectData) {
+ return (
+
+
+
+
+
+
+
+ {title}
+
+ {date}
+
+
+
+
+ );
+}
+
+function ShowCard({ title, subtitle, image, avatar, vip }: ShowData) {
+ return (
+
+
+
+
+
+
+
+ {avatar}
+
+
+ {title}
+
+ {vip ? (
+
+ VIP
+
+ ) : null}
+
+
+ {subtitle}
+
+
+
+ );
+}
+
+function ProjectsSection({
+ className = "",
+ onAllProjectsClick,
+ onBackClick,
+}: {
+ className?: string;
+ onBackClick?: () => void;
+ onAllProjectsClick?: () => void;
+}) {
+ const [projects, setProjects] = useState([]);
+ return (
+
+
+
+ {onBackClick && (
+
+
+ 返回
+
+ )}
+
+ 最近项目
+
+
+ {!!onAllProjectsClick && (
+
+ )}
+
+
+
+
+
+ 开始创作
+
+
+ {projects.map((project) => (
+
+ ))}
+ {onAllProjectsClick && projects.length === 4 && (
+ <>
+
+
+ >
+ )}
+
+
+ );
+}
+
+
+function mapTemplateToShow(item: CanvasTemplateItem, index: number): 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,
+ };
+}
+
+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;
+
+ 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, index) => mapTemplateToShow(item, index))
+ );
+ } catch {
+ if (!controller.signal.aborted) {
+ setDataList([]);
+ }
+ }
+ }, 250);
+
+ return () => {
+ window.clearTimeout(timeout);
+ controller.abort();
+ };
+ }, [searchQuery, selectedCategoryId]);
+
+ 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/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
From 6ac9507da6f505d32664be97084cedacd5729b4d Mon Sep 17 00:00:00 2001
From: huangmin <2927933426@qq.com>
Date: Thu, 4 Jun 2026 10:27:00 +0800
Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E9=A1=B9?=
=?UTF-8?q?=E7=9B=AE=E5=88=97=E8=A1=A8=E5=B1=95=E7=A4=BA=E4=B8=8E=E6=96=B0?=
=?UTF-8?q?=E5=BB=BA=E7=94=BB=E5=B8=83=E5=B7=A5=E4=BD=9C=E6=B5=81=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
1. 扩展CanvasWorkflowListItem接口,新增多个业务字段
2. 重构HomeProjectsContent组件,接入后端API加载项目列表
3. 新增项目创建逻辑,支持从空白和模板快速创建画布项目
4. 优化项目卡片样式,添加点击跳转和空状态占位图
5. 修复模板映射逻辑,完善加载状态与错误处理
---
public/empty_project.png | Bin 0 -> 2159 bytes
src/components/HomeProjectsContent.tsx | 179 ++++++++++++++++++++++---
src/lib/canvasWorkflowApi.ts | 6 +
3 files changed, 164 insertions(+), 21 deletions(-)
create mode 100644 public/empty_project.png
diff --git a/public/empty_project.png b/public/empty_project.png
new file mode 100644
index 0000000000000000000000000000000000000000..978987c3d43ee84bfc89bf23b941c91d78f3bef1
GIT binary patch
literal 2159
zcmdUvYdq5n1IPc#rFC4!aFjXmr13e8ldM7@qP7s`}=f9v&*aHCopm3i$003u&>Hh)%0CWxIp9}zvM*n3GA!V=_0Mt0>b1pt<)hpBS-gQAx
z`r4t6Y(1=Pp}1fi8ILdCe*6I(w|8QHku8Ni8QS_O?_)4sCV;Zdix}R^1Xfdx24{
zKGjT6xXK$lN
zWzc1E`P8OoXi@ofR4{v}?a4}*ytTXKMUeG4!ky!qWoDi1aD_U(#OL#uJB7dPoUV_q
zY`^B?`=||m!bmgVpurPeqV}LvzWgcfq_@#6tD^a_gbvm(U5@r;0(+Z9a5&Q}apVHm
zPHB@kVR!q*WSmbKf9=!j5T5Mk4-GW@?o~xg+D(mIdt$=2|1M{hqAm1v9P!q7ql{2~
zPdyBCx))JVP-oY3Blt(@cJ;=gEd;?}V>F@TG$?-^N8Maod-h28;a7QRo#4RH%f1Fu
zqb)=@(^c$bOiQ-if`>C*z4U{Nkd=*AMe4yW7~Fs?oe^oP=3=^-BH0j$JkX`j9goW+
zrx=UB9@9KQ8u4YnfYN6r>LExBPGlH+aj0#K#Ud_sPedWOMNJlYAPgVdvVlu~dGvVz
zWp1aZ&N2pjrSn91HpHFv*s_2Jc}1IDb)8B5qzvug)F=+zyuZHJrq|0$KcJOO*HG`^
zP=?yZ6bc13`P&Hpn699orj%#&t{!o6!8}k9TE}Bjl57IK^vUE4ti^S14z7xUY#v|Y
zj%+yoKpdQ-J6+4;{2tOpCzF}DS3PObl1;Id;aBcWEMA#=Gifv$mGlsD>md{2^v-l&c00we
zb$cK}%WUyvI1@6=m&BkPN4zT=3wT>|X;8W(eRt^y!==P|f&JLr&1!K>P~jdf-s#-k9ISP0Tjr{B(B
zoOZ+D=2m-WLwdh?)_JRtt<8f2Gx@ee?Iftc+|`5v?R3?EJ<(VxA&HD(GkF;LBV$4lbS
z&Xj;5Q&XYGJi)3G#}W`*JK3;M?PwK%KLC09e-W|2>8o6$@8e6)+q)KI0002Ao5#79
zv!U79<6pydiFfjm^2l_h+?C-9FV!-Rk+}&inJ`LG1DC4qETuK3z+D
zDPc-xP96GwdRo4t`b2;oZi1-u1=HvIuFzj0vX-#535kg*I2;*eF6piR{GP|FCn)7z
z6S5Bi>Am};M^e(0_Q5Tn7_X0
EPa;dX>i_@%
literal 0
HcmV?d00001
diff --git a/src/components/HomeProjectsContent.tsx b/src/components/HomeProjectsContent.tsx
index 2853b1b2..1a9c2588 100644
--- a/src/components/HomeProjectsContent.tsx
+++ b/src/components/HomeProjectsContent.tsx
@@ -1,7 +1,7 @@
"use client";
-import Link from "next/link";
-import { useEffect, useState, type ReactNode } from "react";
+import { useRouter } from "next/navigation";
+import { useCallback, useEffect, useState, type ReactNode } from "react";
import { ProjectActionsDropdown } from "@/components/ProjectActionsDropdown";
import {
ArrowIcon,
@@ -10,8 +10,14 @@ import {
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;
@@ -23,6 +29,7 @@ type ShowData = {
image: string;
avatar: string;
vip?: boolean;
+ data: string;
};
type TemplateCategoryData = {
@@ -42,6 +49,7 @@ type CanvasTemplateItem = {
categoryId: number;
type: number;
coverUrl: string;
+ properties: string;
name: string;
desp: string;
deleted: boolean;
@@ -59,17 +67,53 @@ type HomeProjectsContentProps = {
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 (
-
-
-
+
-
- {title}
-
+
{date}
@@ -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 (
-
+ onOpen({ title, subtitle, image, avatar, vip, data })}>
-
+
@@ -115,7 +159,67 @@ function ProjectsSection({
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 (
@@ -145,17 +249,28 @@ function ProjectsSection({
)}
-
开始创作
-
+
+ {isLoadingProjects && projects.length === 0 ? (
+
+ Loading...
+
+ ) : null}
{projects.map((project) => (
-
+ router.push(`/canvas?workflowId=${project.id}`)}
+ />
))}
{onAllProjectsClick && projects.length === 4 && (
<>
@@ -176,13 +291,14 @@ function ProjectsSection({
}
-function mapTemplateToShow(item: CanvasTemplateItem, index: number): ShowData {
+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 || '',
};
}
@@ -194,6 +310,9 @@ function ShowsSection() {
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;
@@ -257,7 +376,7 @@ function ShowsSection() {
setDataList(
list
.filter((item) => !item.deleted)
- .map((item, index) => mapTemplateToShow(item, index))
+ .map((item) => mapTemplateToShow(item))
);
} catch {
if (!controller.signal.aborted) {
@@ -272,6 +391,24 @@ function ShowsSection() {
};
}, [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 (
@@ -308,7 +445,7 @@ function ShowsSection() {
{dataList.map((show) => (
-
+
))}
diff --git a/src/lib/canvasWorkflowApi.ts b/src/lib/canvasWorkflowApi.ts
index 05800154..abc6195f 100644
--- a/src/lib/canvasWorkflowApi.ts
+++ b/src/lib/canvasWorkflowApi.ts
@@ -19,13 +19,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 {
- {title} -
-{date}
-- {title} -
- {vip ? ( - - VIP - - ) : null} -- {subtitle} -
-- 最近项目 -
- - 全部项目- TV show -
-+ {title} +
+{date}
++ {title} +
+ {vip ? ( + + VIP + + ) : null} ++ {subtitle} +
++ 最近项目 +
++ TV show +
+- {title} -
+{date}