Files
ttstd-infinite-canvas/web/src/pages/create/creation-workspace-empty.tsx
ddcat666 fe17e3747a fix(web): Web 检查 - 格式化拆分模块并修正源码断言
拆分出的前端模块未按 Prettier 格式化导致 CI 格式检查失败;源码断言改为忽略空白差异,导演台引导组件恢复别名导入。同时移除误提交的分析草稿、pnpm 锁文件和调试测试。
2026-09-30 10:32:17 +08:00

151 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 创作页空态:横幅、推荐提示词与精选作品。
import { useAppearanceStore } from "@/stores/use-appearance-store";
import type { CreationMode } from "./creation-assets";
import { ArrowUp, ChevronDown, Clapperboard, FileText, Image as ImageIcon, Sparkles } from "lucide-react";
import { LayoutGroup, motion, useReducedMotion } from "motion/react";
import { useState } from "react";
import { aceternityMotion } from "@/lib/aceternity-motion";
import { creationFeaturedWorks, inspirationSource } from "./creation-inspirations";
import { modeLabels } from "./creation-types";
import { Button } from "antd";
export const creationEmptyBannerFrames = [
{ src: "/short-drama-styles/cyberpunk-neon.jpg", caption: "镜头01 · 雨夜霓虹" },
{ src: "/short-drama-styles/suspense-noir.jpg", caption: "镜头02 · 暗巷追逐" },
{ src: "/short-drama-styles/retro-hong-kong.jpg", caption: "镜头03 · 天台重逢" },
];
export function CreationEmptyBanner() {
const brandName = useAppearanceStore((state) => state.appearance.brandName);
return (
<div className="creation-empty-art" aria-hidden="true">
{creationEmptyBannerFrames.map((frame, index) => (
<figure key={frame.caption} className={`creation-empty-art-frame ${index === 1 ? "is-main" : index === 0 ? "is-back" : "is-front"}`}>
<img src={frame.src} alt="" />
<span>{frame.caption}</span>
</figure>
))}
<span className="creation-empty-art-caption">
<span>{brandName}</span>把每一帧,交给镜头导演
</span>
</div>
);
}
export const creationEmptySuggestions: Array<{ mode: CreationMode; icon: typeof Clapperboard; title: string; hint: string; prompt: string; openLibrary?: boolean }> = [
{ mode: "video", icon: Clapperboard, title: "生成第一个镜头", hint: "描述画面、镜头运动与光线", prompt: "雨夜天台,镜头缓缓推近霓虹灯牌下的主角,她回眸看向镜头,强对比电影感布光" },
{ mode: "image", icon: ImageIcon, title: "从参考图开始", hint: "上传风格图,生成同风格画面", prompt: "", openLibrary: true },
{ mode: "text", icon: FileText, title: "续写故事", hint: "和 AI 讨论剧情、角色与对白", prompt: "帮我续写一个短剧故事,先聊聊剧情走向:" },
{ mode: "video", icon: Sparkles, title: "引用技能增强", hint: "@技能 调用分镜、配音等专业能力", prompt: "调用分镜技能,帮我规划这个镜头的拍摄方案:" },
];
export function CreationEmptySuggest({ onStartPrompt, onOpenLibrary }: { onStartPrompt: (mode: CreationMode, prompt: string) => void; onOpenLibrary: () => void }) {
const reducedMotion = useReducedMotion();
const [hovered, setHovered] = useState<string | null>(null);
return (
<LayoutGroup id="creation-empty-suggest">
<div className="creation-empty-suggest" aria-label="快捷创作入口">
{creationEmptySuggestions.map((item) => {
const Icon = item.icon;
const start = () => {
if (item.openLibrary) onOpenLibrary();
else onStartPrompt(item.mode, item.prompt);
};
return (
<motion.button
key={item.title}
type="button"
className="suggest-card"
onClick={start}
onHoverStart={() => setHovered(item.title)}
onHoverEnd={() => setHovered(null)}
whileHover={reducedMotion ? undefined : { y: -2 }}
transition={aceternityMotion.spring.surface}
>
{hovered === item.title ? <motion.span layoutId="creation-suggest-hover" className="suggest-card-hover" aria-hidden transition={reducedMotion ? { duration: 0 } : aceternityMotion.spring.surface} /> : null}
<span className="library-icon-tile suggest-icon">
<Icon size={18} strokeWidth={2} />
</span>
<span className="suggest-copy">
<strong>{item.title}</strong>
<span>{item.hint}</span>
</span>
</motion.button>
);
})}
</div>
</LayoutGroup>
);
}
export function CreationFeaturedWorks({ onStartPrompt }: { onStartPrompt: (mode: CreationMode, prompt: string) => void }) {
const [filter, setFilter] = useState<"all" | CreationMode>("all");
const [limit, setLimit] = useState(12);
const filtered = creationFeaturedWorks.filter((item) => filter === "all" || item.mode === filter);
return (
<section className="creation-featured-works" aria-labelledby="creation-featured-title">
<div className="creation-featured-heading">
<div>
<h2 id="creation-featured-title">精选灵感</h2>
</div>
<p>{creationFeaturedWorks.length} 个创意起点 · 点击填入提示词,不自动生成</p>
</div>
<div className="creation-inspiration-filters" role="group" aria-label="灵感类型">
{(["all", "video", "image", "text"] as const).map((value) => (
<button
key={value}
type="button"
aria-pressed={filter === value}
onClick={() => {
setFilter(value);
setLimit(12);
}}
>
{value === "all" ? "全部灵感" : modeLabels[value]}
<span>{creationFeaturedWorks.filter((item) => value === "all" || item.mode === value).length}</span>
</button>
))}
</div>
<div className="creation-featured-layout">
{filtered.slice(0, limit).map((item, index) => (
<button key={item.title} type="button" className={`product-collection-card creation-featured-card ${index === 0 ? "is-featured-hero" : ""}`} onClick={() => onStartPrompt(item.mode, item.prompt)}>
<span className="creation-featured-media">
<img src={item.image} alt="" loading="lazy" />
<span className="creation-inspiration-overlay">
<ArrowUp />
使用这个创意
</span>
</span>
<span className="creation-featured-copy">
<strong>{item.title}</strong>
<span>{item.description}</span>
<em>
<Sparkles />
{item.source ? "开源改编 · CC0" : "原创提示词"} · {modeLabels[item.mode]}
</em>
</span>
</button>
))}
</div>
<footer className="creation-inspiration-footer">
{limit < filtered.length ? (
<Button onClick={() => setLimit((count) => count + 12)}>
展开更多灵感
<ChevronDown />
</Button>
) : (
<span>已展示全部 {filtered.length} 个创意</span>
)}
<details>
<summary>模板与封面来源</summary>
<p>{inspirationSource.notice}</p>
<a href={inspirationSource.repository} target="_blank" rel="noreferrer">
awesome-chatgpt-prompts · CC0
</a>
</details>
</footer>
</section>
);
}