import { ArrowLeftOutlined, CheckCircleOutlined, DeleteOutlined, PlayCircleOutlined, SearchOutlined, ShoppingOutlined, TagsOutlined, } from "@ant-design/icons"; import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react"; import "../../styles/pages/more-tools.css"; import "../../styles/pages/image-workbench.css"; import "../../styles/pages/ecommerce.css"; import "../../styles/pages/local-theme-parity.css"; import type { WebProjectSummary } from "../../types"; import { useDebounce } from "../../hooks/useDebounce"; import { templateCarouselCases, templateCases, templateCategories, type TemplateCase } from "./ecommerceTemplates"; interface EcommerceTemplatesPageProps { projects: WebProjectSummary[]; onOpenMore?: () => void; onOpenEcommerce?: () => void; onSelectTemplate?: (template: TemplateCase) => void; onStartCreate?: () => void; onOpenProject: (project: WebProjectSummary) => void; onDeleteProject?: (project: WebProjectSummary) => void; } const APPLE_CAROUSEL_SLOTS = [-4, -3, -2, -1, 0, 1, 2, 3, 4]; const APPLE_CAROUSEL_TRANSITION_MS = 760; interface AppleCarouselMotion { direction: number; progress: 0 | 1; } function getPositiveModulo(value: number, length: number) { return ((value % length) + length) % length; } function getAppleCarouselCardStyle(offset: number): CSSProperties { const depth = Math.abs(offset); const direction = Math.sign(offset); const isActive = depth === 0; const xByDepth = [0, 286, 456, 610, 735, 840]; const yByDepth = [8, -2, -8, -13, -18, -24]; const rotateByDepth = [0, 0, 0, 0, 0]; const scaleByDepth = [1, 0.98, 0.94, 0.91, 0.88, 0.84]; const x = direction * (xByDepth[depth] ?? xByDepth[xByDepth.length - 1]!); const y = yByDepth[depth] ?? yByDepth[yByDepth.length - 1]!; const z = isActive ? 90 : 28 - depth; const rotateY = 0; const rotateZ = direction * (rotateByDepth[depth] ?? rotateByDepth[rotateByDepth.length - 1]!); const scale = scaleByDepth[depth] ?? scaleByDepth[scaleByDepth.length - 1]!; return { "--apple-card-offset": offset, "--apple-card-depth": depth, "--apple-card-z": 80 - depth, "--apple-card-x": `${x}px`, "--apple-card-y": `${y}px`, "--apple-card-z-offset": `${z}px`, "--apple-card-rotate-y": `${rotateY}deg`, "--apple-card-rotate-z": `${rotateZ}deg`, "--apple-card-scale": String(scale), "--apple-card-opacity": String(depth > 4 ? 0 : 1), } as CSSProperties; } function EcommerceTemplatesPage({ projects, onOpenMore, onOpenEcommerce, onSelectTemplate, onStartCreate, onOpenProject, onDeleteProject, }: EcommerceTemplatesPageProps) { const [activeTemplateCategory, setActiveTemplateCategory] = useState("全部"); const [templateSearch, setTemplateSearch] = useState(""); const debouncedSearch = useDebounce(templateSearch, 300); const [carouselIndex, setCarouselIndex] = useState(0); const [carouselMotion, setCarouselMotion] = useState(null); const [carouselIsResetting, setCarouselIsResetting] = useState(false); const carouselFrameRef = useRef(null); const carouselResetFrameRef = useRef(null); const carouselTimerRef = useRef(null); const filteredTemplates = useMemo(() => { const keyword = debouncedSearch.trim(); return templateCases.filter((item) => { const categoryMatches = activeTemplateCategory === "全部" || item.category === activeTemplateCategory; const keywordMatches = !keyword || item.title.includes(keyword) || item.summary.includes(keyword) || item.category.includes(keyword); return categoryMatches && keywordMatches; }); }, [activeTemplateCategory, debouncedSearch]); const carouselItems = useMemo(() => templateCarouselCases.slice(0, 5), []); const carouselSlotOffsets = useMemo(() => { const direction = carouselMotion?.direction ?? 0; const minSlot = APPLE_CAROUSEL_SLOTS[0]! + Math.min(direction, 0); const maxSlot = APPLE_CAROUSEL_SLOTS[APPLE_CAROUSEL_SLOTS.length - 1]! + Math.max(direction, 0); return Array.from({ length: maxSlot - minSlot + 1 }, (_, index) => minSlot + index); }, [carouselMotion?.direction]); const startCarouselShift = useCallback( (rawDirection: number) => { const direction = Math.sign(rawDirection); if (!direction || carouselItems.length <= 1 || carouselMotion) return; if (carouselFrameRef.current !== null) { window.cancelAnimationFrame(carouselFrameRef.current); } if (carouselTimerRef.current !== null) { window.clearTimeout(carouselTimerRef.current); } if (carouselResetFrameRef.current !== null) { window.cancelAnimationFrame(carouselResetFrameRef.current); } setCarouselIsResetting(false); setCarouselMotion({ direction, progress: 0 }); carouselFrameRef.current = window.requestAnimationFrame(() => { carouselFrameRef.current = window.requestAnimationFrame(() => { setCarouselMotion((current) => (current?.direction === direction ? { direction, progress: 1 } : current)); }); }); carouselTimerRef.current = window.setTimeout(() => { setCarouselIsResetting(true); setCarouselIndex((current) => getPositiveModulo(current + direction, carouselItems.length)); setCarouselMotion(null); carouselResetFrameRef.current = window.requestAnimationFrame(() => { carouselResetFrameRef.current = window.requestAnimationFrame(() => { setCarouselIsResetting(false); }); }); }, APPLE_CAROUSEL_TRANSITION_MS); }, [carouselItems.length, carouselMotion], ); useEffect(() => { if (carouselItems.length <= 1) return undefined; const intervalId = window.setInterval(() => { startCarouselShift(-1); }, 2200); return () => window.clearInterval(intervalId); }, [carouselItems.length, startCarouselShift]); useEffect( () => () => { if (carouselFrameRef.current !== null) { window.cancelAnimationFrame(carouselFrameRef.current); } if (carouselTimerRef.current !== null) { window.clearTimeout(carouselTimerRef.current); } if (carouselResetFrameRef.current !== null) { window.cancelAnimationFrame(carouselResetFrameRef.current); } }, [], ); return (
示例模板
{filteredTemplates.length} 个模板 最近项目 {projects.length}
{carouselSlotOffsets.map((slotOffset) => { const itemIndex = getPositiveModulo(carouselIndex + slotOffset, carouselItems.length); const item = carouselItems[itemIndex]; const visualOffset = slotOffset - (carouselMotion?.direction ?? 0) * (carouselMotion?.progress ?? 0); const isActive = visualOffset === 0; if (!item) return null; return ( ); })}
示例模板

常用电商场景

{templateCategories.map((category) => ( ))}
{filteredTemplates.map((item) => (
))}
最近处理

最近项目

{projects.length ? (
{projects.slice(0, 4).map((project, index) => (
{onDeleteProject ? ( ) : null}
))}
) : (
还没有最近项目 完成一次生成后,这里会显示最近处理的商品内容。
)}
); } export default EcommerceTemplatesPage;