添加视频复刻和优化脚本旁白
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user