优化住流程和添加复刻视频页面

This commit is contained in:
Azmat@qq.com
2026-08-26 18:44:03 +08:00
parent 0ee498d807
commit c25060c6c6
30 changed files with 2574 additions and 155 deletions
@@ -31,6 +31,7 @@ const iconPaths: Record<string, string> = {
film: '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 3v18"/><path d="M17 3v18"/><path d="M3 7.5h4"/><path d="M3 16.5h4"/><path d="M17 7.5h4"/><path d="M17 16.5h4"/><path d="M3 12h4"/><path d="M17 12h4"/>',
wand: '<path d="m21.64 3-1.28 1.28a5.5 5.5 0 0 0-7.78 7.78l-8.5 8.5a2.12 2.12 0 0 0 3 3l8.5-8.5a5.5 5.5 0 0 0 7.78-7.78Z"/><path d="m14 7 3 3"/>',
scan: '<path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M7 12h10"/>',
replace: '<path d="M14 4a2 2 0 0 1 2-2"/><path d="M16 10a2 2 0 0 1-2-2"/><path d="M20 2a2 2 0 0 1 2 2"/><path d="M22 8a2 2 0 0 1-2 2"/><path d="m3 7 3 3 3-3"/><path d="M6 10V5a3 3 0 0 1 3-3h1"/><rect x="2" y="14" width="8" height="8" rx="2"/>',
swap: '<path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/>',
arrowRight: '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>'
};
+3 -1
View File
@@ -18,6 +18,7 @@ const SHELL_COMMANDS: Command[] = [
{ id: "free-create", group: "导航", label: "自由创作", sub: "AI 视频生成 · 全能参考 / 首尾帧", page: "freeCreate", icon: "film", key: "F" },
{ id: "quick-create", group: "导航", label: "极速成片", sub: "上传商品图片,自动完成整条视频", page: "quickCreate", icon: "wand", key: "Q" },
{ id: "video-remix", group: "导航", label: "提炼提示词", sub: "上传参考视频,提炼可编辑提示词", page: "videoRemix", icon: "scan", key: "R" },
{ id: "video-replace", group: "导航", label: "视频复刻", sub: "保留参考视频的人物、场景与节奏,复刻出自己的带货内容", page: "videoReplace", icon: "replace", key: "E" },
{ id: "library", group: "导航", label: "成品库", sub: "素材、人物、场景、成片统一管理", page: "library", icon: "folder", key: "A" },
{ id: "team", group: "导航", label: "团队", sub: "成员、权限、额度、协作记录", page: "team", icon: "users" },
{ id: "account", group: "导航", label: "账单库", sub: "余额、充值、账单流水", page: "account", icon: "creditCard" },
@@ -272,7 +273,7 @@ export function topModuleForPage(page: Page): TopModule | null {
|| page === "modelPhotoDemoB"
|| page === "platformCover"
) return "image";
if (page === "projects" || page === "projectWizard" || page === "quickCreate" || page === "pipeline" || page === "freeCreate" || page === "videoRemix") return "video";
if (page === "projects" || page === "projectWizard" || page === "quickCreate" || page === "pipeline" || page === "freeCreate" || page === "videoRemix" || page === "videoReplace") return "video";
return null;
}
@@ -404,6 +405,7 @@ const PAGE_TO_NAV: Partial<Record<Page, Page>> = {
platformCover: "assetFactory",
freeCreate: "freeCreate",
videoRemix: "projects",
videoReplace: "projects",
library: "library",
team: "team",
account: "account",
@@ -1,14 +1,17 @@
import { useEffect, useRef, useState } from "react";
import type { ChangeEvent, DragEvent } from "react";
import { createPortal } from "react-dom";
import { ArrowLeft, Check, ChevronDown, Image as ImageIcon, Plus, ShieldCheck, Trash2, Upload, X } from "lucide-react";
import { ArrowLeft, Check, ChevronDown, ChevronRight, Image as ImageIcon, Plus, ShieldCheck, Trash2, Upload, X } from "lucide-react";
import { api } from "../api";
import { useBodyScrollLock, ConfirmModal } from "./overlays";
import type { Product } from "../types";
import {
BUSINESS_TYPES,
BUSINESS_TYPE_KEYS,
PC_CAT_MORE,
PC_CAT_MORE_LABEL,
PC_CAT_OPTIONS,
PC_CAT_PRIMARY,
catOptionsFor,
type BusinessType,
} from "../product-business";
@@ -20,7 +23,6 @@ export type ProductCreatePayload = {
title: string;
business_type?: BusinessType;
category: string;
target_audience?: string;
selling_points?: Array<{ title: string; detail: string; sort_order: number }>;
cover_asset?: string;
images?: Array<{ asset: string; sort_order: number; is_primary?: boolean }>;
@@ -51,7 +53,6 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
const [title, setTitle] = useState("");
const [businessType, setBusinessType] = useState<BusinessType>("ecommerce");
const [category, setCategory] = useState("");
const [target, setTarget] = useState("");
const [bullets, setBullets] = useState<string[]>([""]);
const [images, setImages] = useState<PfImage[]>([]);
const [dragOver, setDragOver] = useState(false);
@@ -60,6 +61,7 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
const [saving, setSaving] = useState(false);
const [galleryOpen, setGalleryOpen] = useState(false);
const [openSelect, setOpenSelect] = useState<"" | "type" | "cat">("");
const [catMoreOpen, setCatMoreOpen] = useState(false);
const [toast, setToast] = useState<{ title: string; sub: string } | null>(null);
const [confirmExit, setConfirmExit] = useState(false);
const imgInputRef = useRef<HTMLInputElement | null>(null);
@@ -84,7 +86,6 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
setTitle("");
setBusinessType("ecommerce");
setCategory("");
setTarget("");
setBullets([""]);
setImages((list) => { list.forEach((item) => URL.revokeObjectURL(item.url)); return []; });
setDragOver(false);
@@ -92,6 +93,7 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
setCategoryError(false);
setGalleryOpen(false);
setOpenSelect("");
setCatMoreOpen(false);
}
useEffect(() => {
@@ -114,7 +116,7 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
}, [openSelect]);
function isDirty() {
return Boolean(title.trim() || businessType !== "ecommerce" || category || target.trim() || bullets.some((b) => b.trim()) || images.length);
return Boolean(title.trim() || businessType !== "ecommerce" || category || bullets.some((b) => b.trim()) || images.length);
}
function changeBusinessType(next: BusinessType) {
@@ -227,7 +229,6 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
title: name,
business_type: businessType,
category: category || activeCatOptions[0],
target_audience: target.trim() || undefined,
selling_points: finalBullets.map((item, index) => ({ title: item, detail: item, sort_order: index })),
...(images[0]?.assetId
? {
@@ -299,31 +300,57 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
<div className="pcd-field">
<span className="pcd-label"><span> <span className="pcd-req">*</span></span><small></small></span>
<div className={`pcd-select${openSelect === "cat" ? " open" : ""}`}>
<button type="button" className={`pcd-select-trigger${categoryError ? " is-error" : ""}`} aria-haspopup="listbox" aria-expanded={openSelect === "cat"} onClick={() => setOpenSelect((c) => (c === "cat" ? "" : "cat"))}>
<div className={`pcd-select${openSelect === "cat" ? " open" : ""}${catMoreOpen ? " is-more-open" : ""}`}>
<button
type="button"
className={`pcd-select-trigger${categoryError ? " is-error" : ""}`}
aria-haspopup="listbox"
aria-expanded={openSelect === "cat"}
onClick={() => {
if (openSelect === "cat") {
setOpenSelect("");
return;
}
setCatMoreOpen(PC_CAT_MORE.includes(category));
setOpenSelect("cat");
}}
>
<span className={category ? "" : "is-placeholder"}>{category || "请选择商品品类"}</span>
<ChevronDown />
</button>
<div className="pcd-select-menu" role="listbox">
{activeCatOptions.map((option) => (
{isLocal ? activeCatOptions.map((option) => (
<button type="button" className={`pcd-select-option${category === option ? " selected" : ""}`} key={option} onClick={() => { setCategory(option); setCategoryError(false); setOpenSelect(""); }}>
{option}
</button>
))}
)) : (
<>
{PC_CAT_PRIMARY.map((option) => (
<button type="button" className={`pcd-select-option${category === option ? " selected" : ""}`} key={option} onClick={() => { setCategory(option); setCategoryError(false); setOpenSelect(""); }}>
{option}
</button>
))}
<button
type="button"
className={`pcd-select-option pcd-select-more-toggle${catMoreOpen ? " is-open" : ""}`}
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={`pcd-select-option pcd-select-more-item${category === option ? " selected" : ""}`} key={option} onClick={() => { setCategory(option); setCategoryError(false); setOpenSelect(""); }}>
{option}
</button>
)) : null}
</>
)}
</div>
</div>
</div>
</div>
<label className="pcd-field">
<span className="pcd-label"><span></span><small></small></span>
<input
value={target}
onChange={(event) => setTarget(event.target.value)}
placeholder={isLocal ? "例如:附近上班族、带娃家庭、周末聚餐" : "例如:22–32 岁女性、敏感肌、通勤人群"}
/>
</label>
<section className="pcd-block" aria-labelledby="productImageTitle">
<div className="pcd-block-head">
<div>