添加视频复刻和优化脚本旁白

This commit is contained in:
Azmat@qq.com
2026-08-24 10:48:01 +08:00
parent 368586d33e
commit 2f5b7304b0
33 changed files with 1172 additions and 109 deletions
+110 -14
View File
@@ -27,10 +27,47 @@ import { PlatformLibraryModal } from "../components/free-create/platform-library
import type { PromptInputHandle } from "../components/free-create/prompt-input";
import { SystemLoading } from "../components/loading";
import { ConfirmModal } from "../components/overlays";
import { REMIX_PROMPT_KEY } from "./video-remix";
const PAGE_SIZE = 20;
const PROGRESS_KEY = "fc-progress";
type FeedFilter = "all" | "inflight" | "succeeded" | "failed";
const FEED_FILTERS: Array<{ key: FeedFilter; label: string }> = [
{ key: "all", label: "全部" },
{ key: "inflight", label: "生成中" },
{ key: "succeeded", label: "已完成" },
{ key: "failed", label: "失败" }
];
const GROUP_LABEL: Record<"today" | "yesterday" | "earlier", string> = {
today: "今天",
yesterday: "昨天",
earlier: "更早"
};
function chinaDay(iso: string | null) {
if (!iso) return "";
return new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit" }).format(new Date(iso));
}
function dayBucket(iso: string | null): "today" | "yesterday" | "earlier" {
const day = chinaDay(iso);
if (!day) return "earlier";
const today = chinaDay(new Date().toISOString());
if (day === today) return "today";
const yesterday = chinaDay(new Date(Date.now() - 86400000).toISOString());
return day === yesterday ? "yesterday" : "earlier";
}
function matchesFeedFilter(task: FreeVideoTask, filter: FeedFilter) {
if (filter === "all") return true;
if (filter === "inflight") return isInFlight(task.status);
if (filter === "succeeded") return task.status === "succeeded";
return task.status === "failed" || task.status === "cancelled";
}
function loadProgress(): Record<string, number> {
try {
return JSON.parse(sessionStorage.getItem(PROGRESS_KEY) || "{}") as Record<string, number>;
@@ -97,12 +134,21 @@ export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled, onBack }
const [submitting, setSubmitting] = useState(false);
const promptRef = useRef<PromptInputHandle | null>(null);
useEffect(() => {
const draft = sessionStorage.getItem(REMIX_PROMPT_KEY);
if (!draft) return;
sessionStorage.removeItem(REMIX_PROMPT_KEY);
window.setTimeout(() => promptRef.current?.setContent(draft, []), 0);
notify("info", "已填入复刻提示词,可改完再生成");
}, [notify]);
// —— 弹窗 ——
const [detailId, setDetailId] = useState<string | null>(null);
const [libraryOpen, setLibraryOpen] = useState(false);
const [platformOpen, setPlatformOpen] = useState(false);
const [libraryTargetRole, setLibraryTargetRole] = useState<"first_frame" | "last_frame" | null>(null);
const [deleteTarget, setDeleteTarget] = useState<FreeVideoTask | null>(null);
const [feedFilter, setFeedFilter] = useState<FeedFilter>("all");
// —— 轮询/进度 ——
const pollTimersRef = useRef(new Map<string, number>());
@@ -522,6 +568,26 @@ export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled, onBack }
setRefs([]);
}, []);
const filterCounts = useMemo(() => ({
all: tasks.length,
inflight: tasks.filter((t) => isInFlight(t.status)).length,
succeeded: tasks.filter((t) => t.status === "succeeded").length,
failed: tasks.filter((t) => t.status === "failed" || t.status === "cancelled").length
}), [tasks]);
const feedGroups = useMemo(() => {
const visible = tasks.filter((t) => matchesFeedFilter(t, feedFilter));
const groups: Array<{ key: "today" | "yesterday" | "earlier"; items: FreeVideoTask[] }> = [
{ key: "today", items: [] },
{ key: "yesterday", items: [] },
{ key: "earlier", items: [] }
];
visible.forEach((task) => {
groups.find((g) => g.key === dayBucket(task.created_at))?.items.push(task);
});
return groups.filter((g) => g.items.length > 0);
}, [tasks, feedFilter]);
// 详情弹窗的上一条/下一条(只在已完成的任务间切换)
const doneTasks = tasks.filter((t) => t.status === "succeeded" && t.video_url);
const detailTask = detailId ? tasks.find((t) => t.id === detailId) || null : null;
@@ -554,6 +620,22 @@ export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled, onBack }
</div>
</header>
{tasks.length > 0 && (
<div className="fc-feedbar">
{FEED_FILTERS.map((item) => (
<button
key={item.key}
type="button"
className={`fc-filter${feedFilter === item.key ? " active" : ""}`}
onClick={() => setFeedFilter(item.key)}
>
{item.label}
<span>{filterCounts[item.key]}</span>
</button>
))}
</div>
)}
<div className="fc-stage" ref={stageRef}>
{loading ? (
<SystemLoading title="正在加载作品" description="正在同步最新数据,请稍候。" icon="film" />
@@ -563,21 +645,35 @@ export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled, onBack }
<strong></strong>
<p></p>
</div>
) : (
<div className="fc-feed">
{tasks.map((task) => (
<GenerationCard
key={task.id}
task={task}
progress={progress[task.id] || 3}
onOpen={() => setDetailId(task.id)}
onRetry={() => handleRetry(task)}
onToggleFavorite={() => handleFavorite(task)}
onDelete={() => setDeleteTarget(task)}
onDownload={() => void handleDownload(task)}
/>
))}
) : feedGroups.length === 0 ? (
<div className={`fc-empty${hasMore ? " is-soft" : ""}`}>
<div className="fc-empty-icon"><Clapperboard /></div>
<strong></strong>
<p>{hasMore ? "当前已加载的记录里没有,下滑可继续查找" : "换一个状态筛选,或继续在下方生成新视频"}</p>
</div>
) : (
feedGroups.map((group) => (
<section className="fc-group" key={group.key}>
<div className="fc-group-h">
{GROUP_LABEL[group.key]}
<span>{group.items.length}</span>
</div>
<div className="fc-feed">
{group.items.map((task) => (
<GenerationCard
key={task.id}
task={task}
progress={progress[task.id] || 3}
onOpen={() => setDetailId(task.id)}
onRetry={() => handleRetry(task)}
onToggleFavorite={() => handleFavorite(task)}
onDelete={() => setDeleteTarget(task)}
onDownload={() => void handleDownload(task)}
/>
))}
</div>
</section>
))
)}
{hasMore && <div ref={sentinelRef} className="fc-sentinel"></div>}
</div>