2026-06-02 12:38:01 +08:00
|
|
|
import {
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
HeartFilled,
|
|
|
|
|
HeartOutlined,
|
|
|
|
|
ImportOutlined,
|
|
|
|
|
LeftOutlined,
|
|
|
|
|
PictureOutlined,
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
RightOutlined,
|
|
|
|
|
} from "@ant-design/icons";
|
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
|
import { communityClient, type ServerCommunityCase } from "../../api/communityClient";
|
|
|
|
|
import WorkspacePageShell from "../../components/WorkspacePageShell";
|
|
|
|
|
import OptimizedImage from "../../components/OptimizedImage";
|
|
|
|
|
import { EmptyState } from "../../components/EmptyState";
|
|
|
|
|
import { cloneWorkflow, createBlankWorkflow } from "../../data/workflows";
|
|
|
|
|
import type { WebCanvasWorkflow, WebProjectSummary } from "../../types";
|
|
|
|
|
import { getCommunityCaseCover, getWorkflowFromCase, shouldShowInCanvasCommunity } from "./communityCaseUtils";
|
|
|
|
|
import { ossThumb } from "../../utils/ossImageOptimize";
|
2026-06-02 14:34:55 +08:00
|
|
|
const OSS_MUBAN = "https://stringtest.oss-cn-hangzhou.aliyuncs.com/muban";
|
2026-06-02 12:38:01 +08:00
|
|
|
|
|
|
|
|
interface CommunityPageProps {
|
|
|
|
|
projects: WebProjectSummary[];
|
|
|
|
|
isAuthenticated: boolean;
|
|
|
|
|
onStartCreate: () => void;
|
|
|
|
|
onOpenProject: (project: WebProjectSummary) => void;
|
|
|
|
|
onDeleteProject?: (project: WebProjectSummary) => void;
|
|
|
|
|
onImportWorkflow: (workflow: WebCanvasWorkflow) => void;
|
|
|
|
|
onRequireLogin?: (action: string) => boolean | void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const communityCardImages = [
|
2026-06-02 14:34:55 +08:00
|
|
|
`${OSS_MUBAN}/dianshang1.png`,
|
|
|
|
|
`${OSS_MUBAN}/dianshang2.png`,
|
|
|
|
|
`${OSS_MUBAN}/dianshang3.png`,
|
|
|
|
|
`${OSS_MUBAN}/wechat-7.png`,
|
|
|
|
|
`${OSS_MUBAN}/wechat-8.png`,
|
|
|
|
|
`${OSS_MUBAN}/wechat-9.png`,
|
2026-06-02 12:38:01 +08:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const SLIDE_INTERVAL = 3000;
|
|
|
|
|
const CAROUSEL_VISIBLE_COUNT = 3;
|
|
|
|
|
const MANUAL_PAUSE_DURATION = 2000;
|
|
|
|
|
const COMMUNITY_CAROUSEL_VIDEOS = [
|
|
|
|
|
"https://stringtest.oss-cn-hangzhou.aliyuncs.com/test3.mp4",
|
|
|
|
|
"https://stringtest.oss-cn-hangzhou.aliyuncs.com/test4.mp4",
|
|
|
|
|
"https://stringtest.oss-cn-hangzhou.aliyuncs.com/test6.mp4",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function buildWorkflowFromServerCase(item: ServerCommunityCase, fallback: WebCanvasWorkflow): WebCanvasWorkflow {
|
|
|
|
|
const workflow = getWorkflowFromCase(item);
|
|
|
|
|
if (workflow) {
|
|
|
|
|
return cloneWorkflow(workflow);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const coverUrl = getCommunityCaseCover(item);
|
|
|
|
|
return {
|
|
|
|
|
...cloneWorkflow(fallback),
|
|
|
|
|
id: `community-server-${item.id}`,
|
|
|
|
|
title: item.title,
|
|
|
|
|
description: item.description || fallback.description,
|
|
|
|
|
author: item.username || fallback.author,
|
|
|
|
|
source: "community",
|
|
|
|
|
nodes: fallback.nodes.map((node) =>
|
|
|
|
|
node.id === "reference" && coverUrl ? { ...node, previewUrl: coverUrl } : node,
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function CommunityPage({ projects, isAuthenticated, onStartCreate, onOpenProject, onDeleteProject, onImportWorkflow, onRequireLogin }: CommunityPageProps) {
|
|
|
|
|
const [serverCases, setServerCases] = useState<ServerCommunityCase[]>([]);
|
|
|
|
|
const [serverNotice, setServerNotice] = useState<string | null>(null);
|
|
|
|
|
const [favoriteIds, setFavoriteIds] = useState<string[]>([]);
|
|
|
|
|
const canUseProtectedAction = (action: string) => onRequireLogin?.(action) !== false;
|
|
|
|
|
|
|
|
|
|
const handleStartCreate = () => {
|
|
|
|
|
if (!canUseProtectedAction("start-community-project")) return;
|
|
|
|
|
onStartCreate();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleImportWorkflow = (workflow: WebCanvasWorkflow) => {
|
|
|
|
|
if (!canUseProtectedAction("import-community-workflow")) return;
|
|
|
|
|
onImportWorkflow(workflow);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
communityClient
|
|
|
|
|
.listApprovedCases()
|
|
|
|
|
.then((items) => {
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
const canvasItems = items.filter(shouldShowInCanvasCommunity);
|
|
|
|
|
setServerCases(canvasItems);
|
|
|
|
|
setServerNotice(
|
|
|
|
|
canvasItems.length
|
|
|
|
|
? "已连接服务器画布社区"
|
|
|
|
|
: items.length
|
|
|
|
|
? "服务器暂无已审核画布案例"
|
|
|
|
|
: "社区暂无模板",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
setServerCases([]);
|
|
|
|
|
setServerNotice(error instanceof Error ? error.message : "社区服务暂时不可用");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const getProjectTitle = (project: WebProjectSummary, index: number) =>
|
|
|
|
|
project.source === "server" ? project.name : `预览项目 ${index + 1}`;
|
|
|
|
|
|
|
|
|
|
const getProjectSummary = (project: WebProjectSummary) => {
|
|
|
|
|
if (project.source === "server") {
|
|
|
|
|
return project.description || "最近更新的项目";
|
|
|
|
|
}
|
|
|
|
|
return "预览数据";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* Carousel state */
|
|
|
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
|
|
|
const [carouselTransitionEnabled, setCarouselTransitionEnabled] = useState(true);
|
|
|
|
|
const [isHoverPaused, setIsHoverPaused] = useState(false);
|
|
|
|
|
const [isManualPaused, setIsManualPaused] = useState(false);
|
|
|
|
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
|
const manualPauseTimerRef = useRef<number | null>(null);
|
|
|
|
|
|
|
|
|
|
const carouselCases = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
COMMUNITY_CAROUSEL_VIDEOS.map((videoUrl, index) => ({
|
|
|
|
|
title: `社区视频 ${index + 1}`,
|
|
|
|
|
videoUrl,
|
|
|
|
|
})),
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
const totalSlides = carouselCases.length;
|
|
|
|
|
const visibleCarouselCases = useMemo(
|
|
|
|
|
() => [...carouselCases, ...carouselCases.slice(0, CAROUSEL_VISIBLE_COUNT)],
|
|
|
|
|
[carouselCases],
|
|
|
|
|
);
|
|
|
|
|
const activeCarouselIndex = (currentIndex + 1) % totalSlides;
|
|
|
|
|
const centerCarouselItemIndex = currentIndex + Math.floor(CAROUSEL_VISIBLE_COUNT / 2);
|
|
|
|
|
const isAutoPaused = isHoverPaused || isManualPaused;
|
|
|
|
|
|
|
|
|
|
const pauseAutoCarousel = useCallback(() => {
|
|
|
|
|
setIsManualPaused(true);
|
|
|
|
|
if (timerRef.current) {
|
|
|
|
|
clearInterval(timerRef.current);
|
|
|
|
|
timerRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
if (manualPauseTimerRef.current) {
|
|
|
|
|
window.clearTimeout(manualPauseTimerRef.current);
|
|
|
|
|
}
|
|
|
|
|
manualPauseTimerRef.current = window.setTimeout(() => {
|
|
|
|
|
setIsManualPaused(false);
|
|
|
|
|
manualPauseTimerRef.current = null;
|
|
|
|
|
}, MANUAL_PAUSE_DURATION);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const goNext = useCallback((pause = true) => {
|
|
|
|
|
if (pause) pauseAutoCarousel();
|
|
|
|
|
setCarouselTransitionEnabled(true);
|
|
|
|
|
setCurrentIndex((prev) => prev + 1);
|
|
|
|
|
}, [pauseAutoCarousel]);
|
|
|
|
|
|
|
|
|
|
const goPrev = useCallback(() => {
|
|
|
|
|
pauseAutoCarousel();
|
|
|
|
|
setCarouselTransitionEnabled(true);
|
|
|
|
|
setCurrentIndex((prev) => {
|
|
|
|
|
if (prev > 0) return prev - 1;
|
|
|
|
|
|
|
|
|
|
setCarouselTransitionEnabled(false);
|
|
|
|
|
window.requestAnimationFrame(() => {
|
|
|
|
|
setCurrentIndex(totalSlides);
|
|
|
|
|
window.requestAnimationFrame(() => {
|
|
|
|
|
setCarouselTransitionEnabled(true);
|
|
|
|
|
setCurrentIndex(totalSlides - 1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
return prev;
|
|
|
|
|
});
|
|
|
|
|
}, [pauseAutoCarousel, totalSlides]);
|
|
|
|
|
|
|
|
|
|
const handlePrevClick = useCallback(() => {
|
|
|
|
|
goPrev();
|
|
|
|
|
}, [goPrev]);
|
|
|
|
|
|
|
|
|
|
const handleNextClick = useCallback(() => {
|
|
|
|
|
pauseAutoCarousel();
|
|
|
|
|
goNext(false);
|
|
|
|
|
}, [goNext, pauseAutoCarousel]);
|
|
|
|
|
|
|
|
|
|
const goTo = useCallback((index: number) => {
|
|
|
|
|
setCarouselTransitionEnabled(true);
|
|
|
|
|
setCurrentIndex((index - 1 + totalSlides) % totalSlides);
|
|
|
|
|
}, [totalSlides]);
|
|
|
|
|
|
|
|
|
|
const handleCarouselTransitionEnd = () => {
|
|
|
|
|
if (currentIndex < totalSlides) return;
|
|
|
|
|
|
|
|
|
|
setCarouselTransitionEnabled(false);
|
|
|
|
|
setCurrentIndex(0);
|
|
|
|
|
window.requestAnimationFrame(() => {
|
|
|
|
|
setCarouselTransitionEnabled(true);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isAutoPaused) return;
|
|
|
|
|
timerRef.current = setInterval(() => goNext(false), SLIDE_INTERVAL);
|
|
|
|
|
return () => {
|
|
|
|
|
if (timerRef.current) {
|
|
|
|
|
clearInterval(timerRef.current);
|
|
|
|
|
timerRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, [isAutoPaused, goNext]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
if (manualPauseTimerRef.current) {
|
|
|
|
|
window.clearTimeout(manualPauseTimerRef.current);
|
|
|
|
|
}
|
|
|
|
|
if (timerRef.current) {
|
|
|
|
|
clearInterval(timerRef.current);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
const timeoutId = window.setTimeout(() => {
|
|
|
|
|
communityClient
|
|
|
|
|
.listApprovedCases({
|
|
|
|
|
limit: 30,
|
|
|
|
|
sort: "latest",
|
|
|
|
|
})
|
|
|
|
|
.then((items) => {
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
const canvasItems = items.filter(shouldShowInCanvasCommunity);
|
|
|
|
|
setServerCases(canvasItems);
|
|
|
|
|
setServerNotice(
|
|
|
|
|
canvasItems.length
|
|
|
|
|
? "已连接服务器画布社区"
|
|
|
|
|
: items.length
|
|
|
|
|
? "服务器暂无匹配画布案例"
|
|
|
|
|
: "社区暂无模板",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
setServerNotice(error instanceof Error ? error.message : "社区服务暂时不可用");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}, 280);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
window.clearTimeout(timeoutId);
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleToggleFavorite = async (item: ServerCommunityCase, cardId: string) => {
|
|
|
|
|
const nextActive = !(item.isFavorited || favoriteIds.includes(cardId));
|
|
|
|
|
setFavoriteIds((current) =>
|
|
|
|
|
nextActive ? Array.from(new Set([...current, cardId])) : current.filter((id) => id !== cardId),
|
|
|
|
|
);
|
|
|
|
|
setServerCases((current) =>
|
|
|
|
|
current.map((caseItem) =>
|
|
|
|
|
caseItem.id === item.id
|
|
|
|
|
? {
|
|
|
|
|
...caseItem,
|
|
|
|
|
isFavorited: nextActive,
|
|
|
|
|
favoriteCount: Math.max(0, caseItem.favoriteCount + (nextActive ? 1 : -1)),
|
|
|
|
|
}
|
|
|
|
|
: caseItem,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!isAuthenticated || !serverCases.some((caseItem) => caseItem.id === item.id)) return;
|
|
|
|
|
try {
|
|
|
|
|
const stats = await communityClient.setReaction(item.id, "favorite", nextActive);
|
|
|
|
|
setServerCases((current) =>
|
|
|
|
|
current.map((caseItem) => (caseItem.id === item.id ? { ...caseItem, ...stats } : caseItem)),
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setServerNotice(error instanceof Error ? error.message : "收藏状态同步失败");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const liveCases: ServerCommunityCase[] = serverCases.slice(0, 12);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<WorkspacePageShell title="社区" fullWidth className="community-page page-motion">
|
|
|
|
|
<section className="community-hero">
|
|
|
|
|
<div
|
|
|
|
|
className="community-carousel"
|
|
|
|
|
onMouseEnter={() => setIsHoverPaused(true)}
|
|
|
|
|
onMouseLeave={() => setIsHoverPaused(false)}
|
|
|
|
|
role="region"
|
|
|
|
|
aria-label="示例预览轮播"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className={`community-carousel__track${carouselTransitionEnabled ? "" : " is-resetting"}`}
|
|
|
|
|
style={{ transform: `translateX(-${currentIndex * (100 / CAROUSEL_VISIBLE_COUNT)}%)` }}
|
|
|
|
|
onTransitionEnd={handleCarouselTransitionEnd}
|
|
|
|
|
>
|
|
|
|
|
{visibleCarouselCases.map((card, index) => (
|
|
|
|
|
<article
|
|
|
|
|
key={`${card.videoUrl}-${index}`}
|
|
|
|
|
className={`community-carousel__slide community-carousel__slide--video${index === centerCarouselItemIndex ? " is-center" : ""}`}
|
|
|
|
|
aria-hidden={index !== centerCarouselItemIndex}
|
|
|
|
|
>
|
|
|
|
|
<video
|
|
|
|
|
className="community-carousel__video"
|
|
|
|
|
src={card.videoUrl}
|
|
|
|
|
title={card.title}
|
|
|
|
|
autoPlay
|
|
|
|
|
muted
|
|
|
|
|
loop
|
|
|
|
|
playsInline
|
|
|
|
|
preload="metadata"
|
|
|
|
|
/>
|
|
|
|
|
</article>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="community-carousel__arrow community-carousel__arrow--prev"
|
|
|
|
|
onClick={handlePrevClick}
|
|
|
|
|
aria-label="上一张"
|
|
|
|
|
>
|
|
|
|
|
<LeftOutlined />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="community-carousel__arrow community-carousel__arrow--next"
|
|
|
|
|
onClick={handleNextClick}
|
|
|
|
|
aria-label="下一张"
|
|
|
|
|
>
|
|
|
|
|
<RightOutlined />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<div className="community-carousel__dots" role="tablist" aria-label="幻灯片指示器">
|
|
|
|
|
{carouselCases.map((_, index) => (
|
|
|
|
|
<button
|
|
|
|
|
key={index}
|
|
|
|
|
type="button"
|
|
|
|
|
role="tab"
|
|
|
|
|
aria-selected={index === activeCarouselIndex}
|
|
|
|
|
aria-label={`第 ${index + 1} 张`}
|
|
|
|
|
className={index === activeCarouselIndex ? "is-active" : ""}
|
|
|
|
|
onClick={() => goTo(index)}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="community-section">
|
|
|
|
|
<div className="community-section__head">
|
|
|
|
|
<div>
|
|
|
|
|
<h2>最近项目</h2>
|
|
|
|
|
</div>
|
|
|
|
|
{isAuthenticated && <span className="studio-pill">{projects.length} 个项目</span>}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="project-row">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="project-card project-card--button project-card--new"
|
|
|
|
|
onClick={handleStartCreate}
|
|
|
|
|
>
|
|
|
|
|
<PlusOutlined style={{ fontSize: 28 }} />
|
|
|
|
|
<div className="project-card__meta">
|
|
|
|
|
<strong>新建项目</strong>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
{isAuthenticated && projects.map((project, index) => (
|
|
|
|
|
<div key={project.id} className="project-card-shell">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="project-card project-card--button"
|
|
|
|
|
onClick={() => onOpenProject(project)}
|
|
|
|
|
>
|
|
|
|
|
{project.thumbnailUrl ? (
|
|
|
|
|
<OptimizedImage src={ossThumb(project.thumbnailUrl)} alt="" />
|
|
|
|
|
) : (
|
|
|
|
|
<span className="project-card__empty project-card__empty--dark">
|
|
|
|
|
<PictureOutlined />
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
<span className="project-card__caption">{getProjectSummary(project)}</span>
|
|
|
|
|
<div className="project-card__meta">
|
|
|
|
|
<strong>{getProjectTitle(project, index)}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
{onDeleteProject ? (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="project-card__delete"
|
|
|
|
|
aria-label={`删除项目 ${project.name}`}
|
|
|
|
|
onClick={() => onDeleteProject(project)}
|
|
|
|
|
>
|
|
|
|
|
<DeleteOutlined />
|
|
|
|
|
</button>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="community-section">
|
|
|
|
|
<div className="community-section__head">
|
|
|
|
|
<div>
|
|
|
|
|
<h2>社区精选</h2>
|
|
|
|
|
</div>
|
|
|
|
|
{serverNotice ? <span className="studio-pill">{serverNotice}</span> : null}
|
|
|
|
|
</div>
|
|
|
|
|
{liveCases.length ? (
|
|
|
|
|
<div className="community-case-grid community-case-grid--mosaic motion-stagger">
|
|
|
|
|
{liveCases.map((item, index) => {
|
|
|
|
|
const fallbackWorkflow = createBlankWorkflow(item.title);
|
|
|
|
|
const workflow = buildWorkflowFromServerCase(item, fallbackWorkflow);
|
|
|
|
|
const cardId = `case-${item.id}`;
|
|
|
|
|
const isFavorite = item.isFavorited || favoriteIds.includes(cardId);
|
|
|
|
|
const imageUrl = getCommunityCaseCover(item) || communityCardImages[index % communityCardImages.length] || fallbackWorkflow.nodes.find((node) => node.previewUrl)?.previewUrl || "";
|
|
|
|
|
return (
|
|
|
|
|
<article
|
|
|
|
|
key={cardId}
|
|
|
|
|
className={`community-case-card community-case-card--mosaic community-case-card--tile-${index % 8}`}
|
|
|
|
|
onClick={() => handleImportWorkflow(workflow)}
|
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
handleImportWorkflow(workflow);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="community-case-card__preview"
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
handleImportWorkflow(workflow);
|
|
|
|
|
}}
|
|
|
|
|
aria-label={`导入 ${item.title} 工作流`}
|
|
|
|
|
>
|
|
|
|
|
<OptimizedImage src={ossThumb(imageUrl)} alt={item.title} />
|
|
|
|
|
</button>
|
|
|
|
|
<p className="community-case-card__caption">{item.description || "可一键导入工作流继续创作。"}</p>
|
|
|
|
|
<div
|
|
|
|
|
className="community-case-card__body"
|
|
|
|
|
role="button"
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
onClick={() => handleImportWorkflow(workflow)}
|
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
handleImportWorkflow(workflow);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div className="community-case-card__author">
|
|
|
|
|
<span>{item.username || "OmniAI"}</span>
|
|
|
|
|
<span>{item.tags[0] || item.status}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<strong>{item.title}</strong>
|
|
|
|
|
<p>{item.description || "Import this workflow and continue creating."}</p>
|
|
|
|
|
<div className="community-card-actions">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className={isFavorite ? "is-active" : ""}
|
|
|
|
|
aria-pressed={isFavorite}
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
void handleToggleFavorite(item, cardId);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{isFavorite ? <HeartFilled /> : <HeartOutlined />}
|
|
|
|
|
{isFavorite ? "已收藏" : `收藏${item.favoriteCount ? ` ${item.favoriteCount}` : ""}`}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
handleImportWorkflow(workflow);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<ImportOutlined />
|
|
|
|
|
导入工作流
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</article>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<EmptyState
|
|
|
|
|
icon={<ImportOutlined style={{ fontSize: 48 }} />}
|
|
|
|
|
title="社区暂无模板"
|
|
|
|
|
description="管理员审核通过后,画布社区案例会显示在这里。"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section className="community-section community-section--browse-more">
|
|
|
|
|
<button type="button" onClick={handleStartCreate}>
|
|
|
|
|
新建空白画布 <RightOutlined />
|
|
|
|
|
</button>
|
|
|
|
|
</section>
|
|
|
|
|
</WorkspacePageShell>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default CommunityPage;
|