refactor(core): 抽出共享 ProductCreateDrawer · 商品/项目新建复用同一流程
- 新增 components/product-create-drawer.tsx,products/projects 改用同一右侧 Drawer 组件(products -188 / projects -116) - ai-tools 配套调整;附 perf-report 产物 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,8 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import type { CSSProperties, FormEvent } from "react";
|
||||
import type { Asset, Product, Project } from "../types";
|
||||
import type { Page } from "./route-config";
|
||||
import { ConfirmModal, Drawer, EmptyPanel } from "../components/overlays";
|
||||
import { ConfirmModal, EmptyPanel } from "../components/overlays";
|
||||
import { ProductCreateDrawer } from "../components/product-create-drawer";
|
||||
import { Pager } from "../components/pager";
|
||||
import "../project-wizard-page.css";
|
||||
|
||||
@@ -18,8 +19,6 @@ const WIZ_DURATIONS = [
|
||||
];
|
||||
|
||||
const WIZ_PAGE_SIZE = 7; // 4 列 × 2 行 = 8 格,首格为「创建新商品」→ 每页 7 商品
|
||||
// 新建商品抽屉品类选项(与 products.tsx 一致;本组件自洽,不依赖 products.tsx)
|
||||
const WIZ_PC_CATS = ["美妆个护", "服饰内衣", "食品饮料", "家居家电", "数码 3C", "个护清洁", "运动户外", "母婴亲子"];
|
||||
|
||||
type WizProductPayload = {
|
||||
title: string;
|
||||
@@ -28,15 +27,17 @@ type WizProductPayload = {
|
||||
selling_points?: Array<{ title: string; detail: string; sort_order: number }>;
|
||||
};
|
||||
|
||||
export function ProjectWizardPage({ products, projects = [], preselectProductId, onBack, onCreate, onCreateProduct }: {
|
||||
export function ProjectWizardPage({ products, projects = [], preselectProductId, onBack, onCreate, onCreateProduct, onUploadImage }: {
|
||||
products: Product[];
|
||||
projects?: Project[];
|
||||
// 从商品详情页「立即生成」进入时,预勾选该商品(而非默认第一个/耳机);id 不存在则回落默认。
|
||||
preselectProductId?: string;
|
||||
onBack: () => void;
|
||||
onCreate: (payload: { name: string; product: string; metadata?: Record<string, unknown> }) => Promise<unknown> | void;
|
||||
// 「创建新商品」抽屉提交时调用;返回新建商品(含 id)以便自动选中。可选,未接线时抽屉退化为「去商品库」。
|
||||
// 「创建新商品」抽屉提交时调用 → 落库到商品库;返回新建商品(含 id)以便自动选中。可选,未接线时抽屉退化为「去商品库」。
|
||||
onCreateProduct?: (payload: WizProductPayload) => Promise<Product | null | undefined> | void;
|
||||
// 新建商品的主图随商品一起持久化(与商品库新建商品同一流程)
|
||||
onUploadImage?: (productId: string, formData: FormData) => Promise<unknown> | void;
|
||||
}) {
|
||||
// 预选 id 命中商品库时用它,否则默认第一个商品
|
||||
const initialProductId =
|
||||
@@ -56,15 +57,8 @@ export function ProjectWizardPage({ products, projects = [], preselectProductId,
|
||||
const [pickView, setPickView] = useState<"grid" | "list">("grid");
|
||||
const [pickPage, setPickPage] = useState(1);
|
||||
|
||||
// 新建商品 · 右侧抽屉本地态(自洽实现,不依赖 products.tsx)
|
||||
// 新建商品 · 右侧抽屉开关(抽屉实现与商品库共用 ProductCreateDrawer,保证同一流程)
|
||||
const [npOpen, setNpOpen] = useState(false);
|
||||
const [npTitle, setNpTitle] = useState("");
|
||||
const [npCat, setNpCat] = useState(WIZ_PC_CATS[0]);
|
||||
const [npAudience, setNpAudience] = useState("");
|
||||
const [npPoints, setNpPoints] = useState<string[]>([]);
|
||||
const [npDraft, setNpDraft] = useState("");
|
||||
const [npTitleError, setNpTitleError] = useState(false);
|
||||
const [npSaving, setNpSaving] = useState(false);
|
||||
|
||||
// Step 2 · 配置(基础配置只保留项目名 + 成片时长;脚本风格/人物设定已移到流水线第 1 步脚本助手)
|
||||
const [duration, setDuration] = useState<string | null>("0-15");
|
||||
@@ -131,44 +125,9 @@ export function ProjectWizardPage({ products, projects = [], preselectProductId,
|
||||
// 点击「创建新商品」:有 onCreateProduct 接线则开右侧抽屉,否则回落到旧行为(返回去商品库)
|
||||
function openCreateProduct() {
|
||||
if (!onCreateProduct) { onBack(); return; }
|
||||
setNpTitle("");
|
||||
setNpCat(WIZ_PC_CATS[0]);
|
||||
setNpAudience("");
|
||||
setNpPoints([]);
|
||||
setNpDraft("");
|
||||
setNpTitleError(false);
|
||||
setNpOpen(true);
|
||||
}
|
||||
|
||||
function addNpPoint() {
|
||||
const v = npDraft.trim();
|
||||
if (!v || npPoints.includes(v)) { setNpDraft(""); return; }
|
||||
setNpPoints((prev) => [...prev, v]);
|
||||
setNpDraft("");
|
||||
}
|
||||
|
||||
async function submitNewProduct() {
|
||||
if (!onCreateProduct) return;
|
||||
const name = npTitle.trim();
|
||||
if (!name) { setNpTitleError(true); return; } // 空名:必填校验,不静默无反应
|
||||
setNpSaving(true);
|
||||
try {
|
||||
const created = await onCreateProduct({
|
||||
title: name,
|
||||
category: npCat,
|
||||
target_audience: npAudience.trim() || undefined,
|
||||
selling_points: npPoints.map((item, index) => ({ title: item, detail: item, sort_order: index }))
|
||||
});
|
||||
setNpOpen(false);
|
||||
// 新建成功且拿到 id → 自动选中该商品进入第 1 步选中态
|
||||
if (created && typeof created === "object" && "id" in created) {
|
||||
setProductId((created as Product).id);
|
||||
}
|
||||
} finally {
|
||||
setNpSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
const durObj = WIZ_DURATIONS.find((d) => d.id === duration);
|
||||
|
||||
const product1Done = !!productId;
|
||||
@@ -386,56 +345,17 @@ export function ProjectWizardPage({ products, projects = [], preselectProductId,
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* 新建商品 · 右侧抽屉(复用 overlays Drawer;portal 到 body,遮罩盖住头部/侧栏) */}
|
||||
<Drawer title="新建商品" open={npOpen} close={() => setNpOpen(false)}>
|
||||
<div className="field">
|
||||
<label className="field-label">商品名称<span className="req">*</span></label>
|
||||
<input
|
||||
className={`input${npTitleError ? " error" : ""}`}
|
||||
value={npTitle}
|
||||
placeholder="例:南卡 Lite Pro 蓝牙耳机"
|
||||
onChange={(event) => { setNpTitle(event.target.value); if (npTitleError) setNpTitleError(false); }}
|
||||
/>
|
||||
{npTitleError && <div className="field-hint err">// 商品名称不能为空</div>}
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="field-label">商品品类</label>
|
||||
<select className="select" value={npCat} onChange={(event) => setNpCat(event.target.value)}>
|
||||
{WIZ_PC_CATS.map((c) => <option value={c} key={c}>{c}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="field-label">目标人群</label>
|
||||
<input className="input" value={npAudience} placeholder="例:25-30 岁都市白领" onChange={(event) => setNpAudience(event.target.value)} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="field-label">关键卖点</label>
|
||||
<div className="np-point-add">
|
||||
<input
|
||||
className="input"
|
||||
value={npDraft}
|
||||
placeholder="输入一个卖点后回车添加"
|
||||
onChange={(event) => setNpDraft(event.target.value)}
|
||||
onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); addNpPoint(); } }}
|
||||
/>
|
||||
<button className="btn btn-sm" type="button" onClick={addNpPoint}>添加</button>
|
||||
</div>
|
||||
{npPoints.length > 0 && (
|
||||
<div className="theme-pill-row" style={{ marginTop: 10 }}>
|
||||
{npPoints.map((p) => (
|
||||
<button className="theme-pill active" type="button" key={p} onClick={() => setNpPoints((prev) => prev.filter((x) => x !== p))}>
|
||||
<span>{p}</span>
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M4 4l8 8M12 4l-8 8" /></svg>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="np-drawer-foot">
|
||||
<button className="btn" type="button" onClick={() => setNpOpen(false)}>取消</button>
|
||||
<button className="btn btn-primary" type="button" disabled={npSaving} onClick={submitNewProduct}>{npSaving ? "创建中…" : "创建商品"}</button>
|
||||
</div>
|
||||
</Drawer>
|
||||
{/* 新建商品 · 右侧抽屉 · 与商品库新建商品共用同一组件 → 落库到商品库,同一流程 */}
|
||||
{onCreateProduct && (
|
||||
<ProductCreateDrawer
|
||||
open={npOpen}
|
||||
close={() => setNpOpen(false)}
|
||||
onCreate={onCreateProduct}
|
||||
onUploadImage={onUploadImage}
|
||||
// 新建成功 → 自动选中该商品进入第 1 步选中态
|
||||
onCreated={(created) => setProductId(created.id)}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user