优化住流程和添加复刻视频页面
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
ArrowLeft,
|
||||
Boxes,
|
||||
Clapperboard,
|
||||
ChevronRight,
|
||||
ImagePlus,
|
||||
LayoutPanelTop,
|
||||
RefreshCw,
|
||||
@@ -30,6 +31,12 @@ import {
|
||||
type BillingRates,
|
||||
} from "../components/free-create/constants";
|
||||
import type { NavigateFn } from "./route-config";
|
||||
import {
|
||||
PC_CAT_MORE,
|
||||
PC_CAT_MORE_LABEL,
|
||||
PC_CAT_OPTIONS,
|
||||
PC_CAT_PRIMARY,
|
||||
} from "../product-business";
|
||||
|
||||
const PROGRESS_STEPS = [
|
||||
{ label: "脚本", icon: ScrollText },
|
||||
@@ -85,6 +92,10 @@ function historyBadge(item: QuickCreateJob) {
|
||||
return "未完成";
|
||||
}
|
||||
|
||||
function historyCover(item: QuickCreateJob) {
|
||||
return item.product_images?.[0]?.url || "";
|
||||
}
|
||||
|
||||
function historyVideoUrl(item: QuickCreateJob) {
|
||||
return item.result?.video_url || item.result?.video_segments?.[0]?.video_url || "";
|
||||
}
|
||||
@@ -113,6 +124,8 @@ export function QuickCreatePage({
|
||||
modelConfigs: ModelConfig[];
|
||||
}) {
|
||||
const [name, setName] = useState("");
|
||||
const [category, setCategory] = useState("");
|
||||
const [catMoreOpen, setCatMoreOpen] = useState(false);
|
||||
const [images, setImages] = useState<File[]>([]);
|
||||
const [savedImages, setSavedImages] = useState<Array<{ asset_id: string; url: string }>>([]);
|
||||
const [sourceProductId, setSourceProductId] = useState("");
|
||||
@@ -126,7 +139,7 @@ export function QuickCreatePage({
|
||||
const [unavailableMessage, setUnavailableMessage] = useState("");
|
||||
const [pollEpoch, setPollEpoch] = useState(0);
|
||||
const [history, setHistory] = useState<QuickCreateJob[]>([]);
|
||||
const [playing, setPlaying] = useState<{ url: string; poster: string; title: string } | null>(null);
|
||||
const [playing, setPlaying] = useState<{ url: string; title: string } | null>(null);
|
||||
const videoConfigs = useMemo(
|
||||
() => modelConfigs.filter((config) => config.capability === "video" && config.status === "active"),
|
||||
[modelConfigs],
|
||||
@@ -182,6 +195,10 @@ export function QuickCreatePage({
|
||||
productPrefillDoneRef.current = true;
|
||||
setName((current) => current || product.title || "");
|
||||
setSourceProductId((current) => current || product.id);
|
||||
if (product.category && PC_CAT_OPTIONS.includes(product.category)) {
|
||||
setCategory((current) => current || product.category);
|
||||
setCatMoreOpen(PC_CAT_MORE.includes(product.category));
|
||||
}
|
||||
setSavedImages((current) => {
|
||||
if (current.length) return current;
|
||||
return (product.images || [])
|
||||
@@ -299,12 +316,12 @@ export function QuickCreatePage({
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [playing]);
|
||||
|
||||
function playClip(url?: string, poster?: string, title?: string) {
|
||||
function playClip(url?: string, title?: string) {
|
||||
if (!url) {
|
||||
onNotify?.("info", "视频还不能播放,请稍后再试");
|
||||
return;
|
||||
}
|
||||
setPlaying({ url, poster: poster || "", title: title || "预览视频" });
|
||||
setPlaying({ url, title: title || "预览视频" });
|
||||
}
|
||||
|
||||
function selectImages(files: FileList | null) {
|
||||
@@ -372,6 +389,10 @@ export function QuickCreatePage({
|
||||
onNotify?.("info", "请先填写商品名称并上传商品图片");
|
||||
return;
|
||||
}
|
||||
if (!category) {
|
||||
onNotify?.("info", "请选择商品品类,脚本会按品类来写");
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
setJob(null);
|
||||
setServiceUnavailable(false);
|
||||
@@ -381,6 +402,7 @@ export function QuickCreatePage({
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append("name", name.trim());
|
||||
form.append("category", category);
|
||||
form.append("aspect_ratio", aspectRatio);
|
||||
form.append("resolution", resolution);
|
||||
form.append("total_duration", String(totalDuration));
|
||||
@@ -433,6 +455,8 @@ export function QuickCreatePage({
|
||||
|
||||
function clearDraft() {
|
||||
setName("");
|
||||
setCategory("");
|
||||
setCatMoreOpen(false);
|
||||
setImages([]);
|
||||
setSavedImages([]);
|
||||
setSourceProductId("");
|
||||
@@ -501,6 +525,7 @@ export function QuickCreatePage({
|
||||
const reviewBlocked = isReviewFailure(job?.error_message);
|
||||
const displayUrls = [...savedImages.map((image) => image.url), ...imagePreviews];
|
||||
const imageCount = savedImages.length + images.length;
|
||||
const canStart = Boolean(name.trim() && category && imageCount && selectedVideoModel) && !reviewBlocked;
|
||||
const canRetry = Boolean(name.trim() && imageCount && selectedVideoModel) && !reviewBlocked;
|
||||
const activePhase = submitting ? 0 : Math.max(0, Math.min(4, job?.phase_index ?? 0));
|
||||
const result = job?.result;
|
||||
@@ -511,7 +536,6 @@ export function QuickCreatePage({
|
||||
: [];
|
||||
const clipUrls = new Set(videoClips.map((clip) => clip.video_url).filter(Boolean));
|
||||
const mergedUrl = result?.final_video_url || (result?.video_url && !clipUrls.has(result.video_url) ? result.video_url : "");
|
||||
const posterFallback = displayUrls[0] || "";
|
||||
const sceneCount = Math.max(1, Math.round(totalDuration / 15));
|
||||
const videoEstimate = estimateCost(
|
||||
selectedVideoModel,
|
||||
@@ -547,6 +571,46 @@ export function QuickCreatePage({
|
||||
<input className="quick-name-input" autoComplete="off" value={name} onChange={(event) => setName(event.target.value)} placeholder="例如:净颜精华、轻醒咖啡" disabled={isGenerating} />
|
||||
</label>
|
||||
|
||||
<div className="quick-field">
|
||||
<span className="quick-field-label"><span>商品品类</span><small>必选 · 写脚本时会用到</small></span>
|
||||
<div className={`quick-cat-grid${catMoreOpen ? " is-more-open" : ""}`} role="listbox" aria-label="商品品类">
|
||||
{PC_CAT_PRIMARY.map((option) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`chip quick-cat-chip${category === option ? " active" : ""}`}
|
||||
key={option}
|
||||
disabled={isGenerating}
|
||||
aria-selected={category === option}
|
||||
onClick={() => { setCategory(option); setCatMoreOpen(false); }}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={`chip quick-cat-chip quick-cat-more${catMoreOpen ? " is-open" : ""}`}
|
||||
disabled={isGenerating}
|
||||
aria-expanded={catMoreOpen}
|
||||
onClick={() => setCatMoreOpen((open) => !open)}
|
||||
>
|
||||
<span>{PC_CAT_MORE_LABEL}</span>
|
||||
<ChevronRight />
|
||||
</button>
|
||||
{catMoreOpen ? PC_CAT_MORE.map((option) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`chip quick-cat-chip${category === option ? " active" : ""}`}
|
||||
key={option}
|
||||
disabled={isGenerating}
|
||||
aria-selected={category === option}
|
||||
onClick={() => setCategory(option)}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
)) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="quick-field">
|
||||
<span className="quick-field-label"><span>商品图片</span><small>必填 · 最多9张</small></span>
|
||||
<div className={`quick-upload${imageCount ? " has-images" : ""}`}>
|
||||
@@ -613,7 +677,7 @@ export function QuickCreatePage({
|
||||
<X /><span>{cancelling ? "正在取消…" : "取消生成"}</span>
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" className="quick-generate-button" onClick={() => void startGeneration()} disabled={!canRetry}>
|
||||
<button type="button" className="quick-generate-button" onClick={() => void startGeneration()} disabled={!canStart}>
|
||||
<WandSparkles /><span>{`立即生成视频 · 消耗 ${estimatedPoints} 积分`}</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -646,24 +710,23 @@ export function QuickCreatePage({
|
||||
|
||||
<div className="quick-state quick-state-complete">
|
||||
<div className={`quick-video-result-grid${videoClips.length === 1 ? " is-single" : ""}`}>
|
||||
{videoClips.map((clip, index) => (
|
||||
{videoClips.map((clip, index) => {
|
||||
const clipUrl = clip.video_url || result?.video_url || "";
|
||||
return (
|
||||
<button
|
||||
key={clip.id}
|
||||
type="button"
|
||||
className="quick-video-result-card"
|
||||
onClick={() => playClip(clip.video_url || result?.video_url, clip.poster_url || result?.poster_url || posterFallback, `第${index + 1}场视频`)}
|
||||
onClick={() => playClip(clipUrl, `第${index + 1}场视频`)}
|
||||
>
|
||||
<span className="quick-video-result-thumb">
|
||||
{clip.poster_url || result?.poster_url || posterFallback ? (
|
||||
<img src={clip.poster_url || result?.poster_url || posterFallback} alt={`第${index + 1}场视频首帧`} />
|
||||
) : clip.video_url ? (
|
||||
<video src={clip.video_url} muted playsInline preload="metadata" />
|
||||
) : null}
|
||||
{clipUrl ? <video src={clipUrl} muted playsInline preload="metadata" /> : null}
|
||||
<span className="quick-video-play" aria-hidden="true"><Play /></span>
|
||||
</span>
|
||||
<span className="quick-video-result-meta"><strong>第{index + 1}场视频</strong><small>{clip.duration_seconds || 15}秒</small></span>
|
||||
</button>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="quick-result-head">
|
||||
<div>
|
||||
@@ -676,7 +739,7 @@ export function QuickCreatePage({
|
||||
<button type="button" className="secondary-action" onClick={resetResult}><RefreshCw />重新生成</button>
|
||||
{videoClips.length > 1 ? (
|
||||
mergedUrl ? (
|
||||
<button type="button" className="secondary-action" onClick={() => playClip(mergedUrl, result?.poster_url || posterFallback, "完整视频")}>
|
||||
<button type="button" className="secondary-action" onClick={() => playClip(mergedUrl, "完整视频")}>
|
||||
<Play />播放
|
||||
</button>
|
||||
) : (
|
||||
@@ -743,7 +806,7 @@ export function QuickCreatePage({
|
||||
{history.length ? (
|
||||
<div className="quick-history-list">
|
||||
{history.map((item) => {
|
||||
const poster = item.result?.poster_url || "";
|
||||
const cover = historyCover(item);
|
||||
const videoUrl = historyVideoUrl(item);
|
||||
const duration = item.result?.duration_seconds || item.settings?.total_duration || 15;
|
||||
const scenes = item.result?.video_segments?.length || Math.max(1, Math.round(duration / 15));
|
||||
@@ -754,11 +817,11 @@ export function QuickCreatePage({
|
||||
<button
|
||||
type="button"
|
||||
className="quick-history-thumb"
|
||||
onClick={() => playClip(videoUrl, poster, historyTitle(item))}
|
||||
onClick={() => playClip(videoUrl, historyTitle(item))}
|
||||
aria-label={canPlay ? `播放${historyTitle(item)}` : historyTitle(item)}
|
||||
disabled={!canPlay}
|
||||
>
|
||||
{poster ? <img src={poster} alt="" /> : <span className="quick-history-thumb-empty">{canPlay ? null : <Play />}</span>}
|
||||
{cover ? <img src={cover} alt="" /> : <span className="quick-history-thumb-empty">{canPlay ? null : <Play />}</span>}
|
||||
{canPlay ? <span className="quick-history-play" aria-hidden="true"><Play /></span> : null}
|
||||
<small>{formatClock(duration)}</small>
|
||||
</button>
|
||||
@@ -798,7 +861,7 @@ export function QuickCreatePage({
|
||||
<strong>{playing.title}</strong>
|
||||
<button type="button" onClick={() => setPlaying(null)} aria-label="关闭播放"><X /></button>
|
||||
</div>
|
||||
<video src={playing.url} poster={playing.poster || undefined} controls autoPlay playsInline controlsList="nodownload" />
|
||||
<video src={playing.url} controls autoPlay playsInline controlsList="nodownload" />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user