完善二期清单
This commit is contained in:
@@ -4,15 +4,20 @@ import { createPortal } from "react-dom";
|
||||
import { api } from "../api";
|
||||
import { useBodyScrollLock, ConfirmModal } from "./overlays";
|
||||
import type { Product } from "../types";
|
||||
import {
|
||||
BUSINESS_TYPES,
|
||||
BUSINESS_TYPE_KEYS,
|
||||
PC_CAT_OPTIONS,
|
||||
catOptionsFor,
|
||||
type BusinessType,
|
||||
} from "../product-business";
|
||||
import "../product-create-page.css";
|
||||
|
||||
export const PC_CAT_OPTIONS = ["美妆个护", "服饰内衣", "食品饮料", "家居家电", "数码 3C", "个护清洁", "运动户外", "母婴亲子"];
|
||||
|
||||
// 主图上限(对齐 V1 #pc-drawer:PF_MAX = 5)
|
||||
const PF_MAX = 5;
|
||||
export { PC_CAT_OPTIONS };
|
||||
|
||||
export type ProductCreatePayload = {
|
||||
title: string;
|
||||
business_type?: BusinessType;
|
||||
category: string;
|
||||
target_audience?: string;
|
||||
selling_points?: Array<{ title: string; detail: string; sort_order: number }>;
|
||||
@@ -21,6 +26,9 @@ export type ProductCreatePayload = {
|
||||
images?: Array<{ asset: string; sort_order: number; is_primary?: boolean }>;
|
||||
};
|
||||
|
||||
// 主图上限(对齐 V1 #pc-drawer:PF_MAX = 5)
|
||||
const PF_MAX = 5;
|
||||
|
||||
// 抽屉内主图条目:拖入即传 /assets/upload/ 进桶,assetId 是返回的素材 id;url 仅作本地缩略预览
|
||||
type PfImage = { id: string; file: File; url: string; assetId?: string; status: "uploading" | "done" | "error" };
|
||||
|
||||
@@ -40,7 +48,8 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
|
||||
}) {
|
||||
useBodyScrollLock(open);
|
||||
const [title, setTitle] = useState("");
|
||||
const [category, setCategory] = useState("");
|
||||
const [businessType, setBusinessType] = useState<BusinessType>("ecommerce");
|
||||
const [category, setCategory] = useState(PC_CAT_OPTIONS[0]);
|
||||
const [target, setTarget] = useState("");
|
||||
// 卖点 = 一组可直接编辑的输入行(每行一条);点「添加卖点」按钮新增一条空行,
|
||||
// 不再靠「在一个输入框里回车隐晦追加」(YYX#17)。
|
||||
@@ -59,6 +68,8 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
|
||||
// 始终指向最新 images:addImages 里同步算余量 + 发上传,不靠会滞后的 effect/闭包
|
||||
const imagesRef = useRef<PfImage[]>([]);
|
||||
imagesRef.current = images;
|
||||
const isLocal = businessType === "local_life";
|
||||
const activeCatOptions = isLocal ? catOptionsFor(businessType) : (catOptions.length ? catOptions : PC_CAT_OPTIONS);
|
||||
|
||||
function flash(title: string, sub: string) {
|
||||
if (toastTimer.current) clearTimeout(toastTimer.current);
|
||||
@@ -72,7 +83,8 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
|
||||
|
||||
function resetForm() {
|
||||
setTitle("");
|
||||
setCategory("");
|
||||
setBusinessType("ecommerce");
|
||||
setCategory(PC_CAT_OPTIONS[0]);
|
||||
setTarget("");
|
||||
setBullets([]);
|
||||
setImages((list) => { list.forEach((item) => URL.revokeObjectURL(item.url)); return []; });
|
||||
@@ -84,7 +96,13 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
|
||||
|
||||
// YYX#24:表单是否已填内容(有任一内容即视为「编辑中」)
|
||||
function isDirty() {
|
||||
return Boolean(title.trim() || target.trim() || bullets.some((b) => b.trim()) || images.length);
|
||||
return Boolean(title.trim() || businessType !== "ecommerce" || target.trim() || bullets.some((b) => b.trim()) || images.length);
|
||||
}
|
||||
|
||||
function changeBusinessType(next: BusinessType) {
|
||||
setBusinessType(next);
|
||||
const nextOpts = catOptionsFor(next);
|
||||
if (!nextOpts.includes(category)) setCategory(nextOpts[0]);
|
||||
}
|
||||
// 关闭入口统一走这里:有内容则先二次确认,否则直接关
|
||||
function guardedClose() {
|
||||
@@ -166,7 +184,8 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
|
||||
async function submit() {
|
||||
const name = title.trim();
|
||||
if (!name) { setTitleError(true); flash("请填写商品名称", "必填项"); return; } // 空名:必填校验,不静默无反应
|
||||
if (images.length === 0) { flash("请上传商品主图", "至少 1 张 · 必填"); return; }
|
||||
// 电商必须有主图;本地生活团购是虚拟商品,允许不传图。
|
||||
if (!isLocal && images.length === 0) { flash("请上传商品主图", "至少 1 张 · 必填"); return; }
|
||||
// upload-first:图必须先全部传完进桶(拿到 assetId)才能带进创建
|
||||
if (images.some((im) => im.status === "uploading")) { flash("图片上传中", "请等图片传完再创建"); return; }
|
||||
if (images.some((im) => im.status === "error" || !im.assetId)) { flash("有图片未传成功", "请移除失败的图后重试"); return; }
|
||||
@@ -179,11 +198,16 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
|
||||
// 图已在桶里,创建时直接带上 cover_asset(主图)+ images(全部),一步落库、不再二次传图
|
||||
const created = (await onCreate({
|
||||
title: name,
|
||||
category: category || catOptions[0],
|
||||
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 })),
|
||||
cover_asset: images[0].assetId,
|
||||
images: images.map((im, i) => ({ asset: im.assetId as string, sort_order: i, is_primary: i === 0 }))
|
||||
...(images[0]?.assetId
|
||||
? {
|
||||
cover_asset: images[0].assetId,
|
||||
images: images.map((im, i) => ({ asset: im.assetId as string, sort_order: i, is_primary: i === 0 })),
|
||||
}
|
||||
: {}),
|
||||
})) || null;
|
||||
if (!created) { flash("商品创建失败", "请稍后重试"); return; } // 不静默:建库失败也给提示
|
||||
close();
|
||||
@@ -212,21 +236,29 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
|
||||
{titleError && <div className="pc-err-note">请先填写商品名称</div>}
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label className="field-label">类目<span className="req">*</span></label>
|
||||
<select className="select" value={businessType} onChange={(event) => changeBusinessType(event.target.value as BusinessType)}>
|
||||
{BUSINESS_TYPE_KEYS.map((key) => <option key={key} value={key}>{BUSINESS_TYPES[key]}</option>)}
|
||||
</select>
|
||||
{isLocal && <div className="pc-type-hint mono">// 团购按虚拟商品走主流程,无需实物三视图</div>}
|
||||
</div>
|
||||
|
||||
<div className="field-row">
|
||||
<div>
|
||||
<label className="field-label">品类<span className="req">*</span></label>
|
||||
<select className="select" value={category} onChange={(event) => setCategory(event.target.value)}>
|
||||
{catOptions.map((option) => <option key={option}>{option}</option>)}
|
||||
{activeCatOptions.map((option) => <option key={option}>{option}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="field-label">目标人群<span className="opt">(选填)</span></label>
|
||||
<input className="input" value={target} onChange={(event) => setTarget(event.target.value)} placeholder="例: 22-32 岁女性、敏感肌、办公室通勤" />
|
||||
<input className="input" value={target} onChange={(event) => setTarget(event.target.value)} placeholder={isLocal ? "例: 附近上班族、带娃家庭、周末聚餐" : "例: 22-32 岁女性、敏感肌、办公室通勤"} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label className="field-label">商品主图<span className="req">*</span></label>
|
||||
<label className="field-label">{isLocal ? "门店图 / 套餐海报" : "商品主图"}{isLocal ? <span className="opt">(选填)</span> : <span className="req">*</span>}</label>
|
||||
<input ref={imgInputRef} type="file" accept="image/*" multiple hidden onChange={pickImage} />
|
||||
<div className="pf-upload-row">
|
||||
<div
|
||||
@@ -243,7 +275,7 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12" /></svg>
|
||||
</div>
|
||||
<div className="uz-t">点击上传或<strong>拖拽图片</strong>到此处</div>
|
||||
<div className="uz-d">// 支持 JPG、PNG 格式,建议尺寸 800×800 以上,大小不超过 10MB</div>
|
||||
<div className="uz-d">{isLocal ? "// 店招、套餐海报均可 · 团购无实物可不传" : "// 支持 JPG、PNG 格式,建议尺寸 800×800 以上,大小不超过 10MB"}</div>
|
||||
</div>
|
||||
<div className="pf-example">
|
||||
<div className="ex-h">示例图</div>
|
||||
@@ -252,7 +284,7 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
|
||||
<div className="ex-thumb ex-thumb--img"><img src="/exact/assets/mock/product-mask.png" alt="示例:面膜" loading="lazy" /></div>
|
||||
<div className="ex-thumb ex-thumb--img"><img src="/exact/assets/mock/product-air-fryer.png" alt="示例:空气炸锅" loading="lazy" /></div>
|
||||
</div>
|
||||
<div className="ex-d">优质的商品图有助于生成更好的素材效果</div>
|
||||
<div className="ex-d">{isLocal ? "有门店或套餐图,后续脚本和出图更准" : "优质的商品图有助于生成更好的素材效果"}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pf-grid">
|
||||
@@ -301,7 +333,7 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
|
||||
value={bullet}
|
||||
onChange={(event) => updateBullet(index, event.target.value)}
|
||||
onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); addBullet(); } }}
|
||||
placeholder="例: 玻尿酸双效保湿,4 小时持久水润"
|
||||
placeholder={isLocal ? "例: 双人下午茶套餐,含蛋糕+饮品,周末通用" : "例: 玻尿酸双效保湿,4 小时持久水润"}
|
||||
/>
|
||||
<button className="bl-x" type="button" onClick={() => removeBullet(index)} aria-label="删除卖点">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M6 6l12 12M6 18L18 6" /></svg>
|
||||
@@ -316,8 +348,8 @@ export function ProductCreateDrawer({ open, close, onCreate, onCreated, catOptio
|
||||
{showGuide && (
|
||||
<div className="pc-guide-note">
|
||||
<strong>// 建好商品的 3 步</strong><br />
|
||||
① 填写商品名称 + 品类(必填,用于脚本/素材生成)<br />
|
||||
② 上传清晰主图(800×800 以上),便于 AI 出图更准<br />
|
||||
① 填写商品名称 + 类目(电商/本地生活) + 品类<br />
|
||||
② 电商须上传清晰主图;本地生活/团购无实物可不传<br />
|
||||
③ 写 2–4 条核心卖点,越具体生成效果越好
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user