Main merge work #19

Merged
stringadmin merged 33 commits from main-merge-work into main 2026-06-16 06:38:21 +00:00
2 changed files with 215 additions and 191 deletions
Showing only changes of commit c748d1e3ba - Show all commits
+3 -191
View File
@@ -63,6 +63,7 @@ import {
type CloneOutputKey,
type ProductSetOutputKey,
} from "./utils/platformRules";
import { type CloneSetCountKey, type CloneModelPanelTab, type CloneVideoQualityKey, type CloneReferenceMode, type CloneReplicateLevelKey, type CloneImageItem, type CloneResult, type CloneSavedSetting, type EcommerceHistoryRecord, defaultCloneSetCounts, defaultCloneDetailModuleIds, cloneLatestSettingStorageKey, ecommerceHistoryStorageKey, isCloneSavedSetting, isCloneImageItem, isCloneResult, isEcommerceHistoryRecord, readCloneLatestSetting, writeCloneLatestSetting, clearCloneLatestSetting, readEcommerceHistoryRecords, writeEcommerceHistoryRecords, removeFilePayloadFromImages, normalizeEcommerceHistoryRecord } from "./utils/clonePersistence";
import {
buildEcommerceImagePrompt,
buildSetSubPrompt,
@@ -212,40 +213,16 @@ interface ProductClonePageProps {
[key: string]: unknown;
}
type ProductCloneStatus = "idle" | "ready" | "generating" | "done" | "failed";
type CloneSetCountKey = "selling" | "white" | "scene";
type CloneModelPanelTab = "scene" | "model";
type CloneVideoQualityKey = "standard" | "high" | "ultra";
type ProductSetStatus = "idle" | "ready" | "generating" | "done" | "failed";
type ProductKitToolKey = "set" | "detail" | "wear" | "clone";
type ComposerMenuKey = "mode" | "platform" | "language" | "ratio" | "settings";
type CloneBasicSelectKey = "platform" | "market" | "language" | "ratio";
type CloneModelSelectKey = "gender" | "age" | "ethnicity" | "body";
type CloneReferenceMode = "upload" | "link";
type CloneReplicateLevelKey = "style" | "high";
type ProductCloneStatus = "idle" | "ready" | "generating" | "done" | "failed";
type TryOnModelSource = "ai" | "library";
type TryOnStatus = "idle" | "modeling" | "ready" | "generating" | "done" | "failed";
type DetailStatus = "idle" | "ready" | "generating" | "done" | "failed";
interface CloneImageItem {
id: string;
src: string;
name: string;
file?: File;
width?: number;
height?: number;
format?: string;
mimeType?: string;
ossKey?: string;
}
interface CloneResult {
id: string;
src: string;
label: string;
type?: "image" | "video";
}
interface CanvasNode {
id: string;
mode: string;
@@ -256,52 +233,6 @@ interface CanvasNode {
y: number;
}
interface CloneSavedSetting {
id: string;
name: string;
savedAt: string;
output: CloneOutputKey;
platform: string;
market: string;
language: string;
ratio: string;
setCounts: Record<CloneSetCountKey, number>;
detailModules: string[];
modelPanelTab: CloneModelPanelTab;
modelScenes: string[];
modelCustomScene: string;
modelGender: string;
modelAge: string;
modelEthnicity: string;
modelBody: string;
modelAppearance: string;
videoQuality: CloneVideoQualityKey;
videoDurationSeconds: number;
videoSmart: boolean;
referenceMode?: CloneReferenceMode;
replicateLevel?: CloneReplicateLevelKey;
requirement: string;
}
interface EcommerceHistoryRecord {
id: string;
title: string;
createdAt: number;
output: CloneOutputKey;
platform: string;
market: string;
language: string;
ratio: string;
requirement: string;
productImages: CloneImageItem[];
results: CloneResult[];
setResultImages: string[];
setCounts: Record<CloneSetCountKey, number>;
detailModules: string[];
modelScenes: string[];
referenceImages: CloneImageItem[];
replicateLevel: CloneReplicateLevelKey;
}
interface ProductSetPreviewSelection {
src: string;
@@ -391,19 +322,12 @@ const cloneSetCountOptions: Array<{
{ key: "scene", title: "场景图", desc: "展示商品生活使用场景和人物搭配" },
];
const cloneSetCountKeys = cloneSetCountOptions.map((option) => option.key);
const defaultCloneSetCounts: Record<CloneSetCountKey, number> = {
selling: 3,
white: 1,
scene: 3,
};
const minCloneSetTotal = 1;
const maxCloneSetTotal = 16;
const maxCloneProductImages = 7;
const maxCloneReferenceImages = 20;
const cloneVideoDurationMin = 5;
const cloneVideoDurationMax = 45;
const cloneLatestSettingStorageKey = "omniai.clone-ai.latest-setting";
const ecommerceHistoryStorageKey = "omniai.ecommerce.history.records";
const cloneVideoQualityOptions: Array<{ key: CloneVideoQualityKey; label: string; desc: string }> = [
{ key: "standard", label: "标准", desc: "快速出片" },
{ key: "high", label: "高清", desc: "推荐" },
@@ -482,7 +406,6 @@ const detailModules = [
{ id: "tips", title: "使用提示图", desc: "提醒操作与保养要点" },
];
const defaultDetailModuleIds: string[] = [];
const defaultCloneDetailModuleIds = ["hero", "selling", "usage", "angle", "scene", "detail"];
const maxDetailModuleSelection = 6;
const cloneDetailModules = detailModules;
const detailAssets = ossAssets.ecommerce.detail;
@@ -618,117 +541,6 @@ function notifyRejectedImages(files: File[]): File[] {
return accepted;
}
function isCloneSavedSetting(item: unknown): item is CloneSavedSetting {
const candidate = item as Partial<CloneSavedSetting>;
return (
typeof candidate.id === "string" &&
typeof candidate.name === "string" &&
typeof candidate.savedAt === "string" &&
typeof candidate.output === "string" &&
typeof candidate.platform === "string" &&
typeof candidate.market === "string" &&
typeof candidate.language === "string" &&
typeof candidate.ratio === "string" &&
typeof candidate.videoDurationSeconds === "number"
);
}
function readCloneLatestSetting() {
if (typeof window === "undefined") return null;
try {
const rawValue = window.localStorage.getItem(cloneLatestSettingStorageKey);
if (rawValue) {
const parsedValue: unknown = JSON.parse(rawValue);
if (isCloneSavedSetting(parsedValue)) return parsedValue;
}
} catch {
return null;
}
return null;
}
function writeCloneLatestSetting(setting: CloneSavedSetting) {
if (typeof window === "undefined") return;
window.localStorage.setItem(cloneLatestSettingStorageKey, JSON.stringify(setting));
}
function isCloneImageItem(item: unknown): item is CloneImageItem {
const candidate = item as Partial<CloneImageItem>;
return typeof candidate.id === "string" && typeof candidate.src === "string" && typeof candidate.name === "string";
}
function isCloneResult(item: unknown): item is CloneResult {
const candidate = item as Partial<CloneResult>;
return typeof candidate.id === "string" && typeof candidate.src === "string" && typeof candidate.label === "string";
}
function isEcommerceHistoryRecord(item: unknown): item is EcommerceHistoryRecord {
const candidate = item as Partial<EcommerceHistoryRecord>;
return (
typeof candidate.id === "string" &&
typeof candidate.title === "string" &&
typeof candidate.createdAt === "number" &&
typeof candidate.output === "string" &&
typeof candidate.platform === "string" &&
typeof candidate.market === "string" &&
typeof candidate.language === "string" &&
typeof candidate.ratio === "string" &&
typeof candidate.requirement === "string" &&
Array.isArray(candidate.productImages) &&
candidate.productImages.every(isCloneImageItem) &&
Array.isArray(candidate.results) &&
candidate.results.every(isCloneResult)
);
}
function removeFilePayloadFromImages(images: CloneImageItem[]): CloneImageItem[] {
return images.map(({ id, src, name, width, height, format, mimeType, ossKey }) => ({
id,
src,
name,
width,
height,
format,
mimeType,
ossKey,
}));
}
function normalizeEcommerceHistoryRecord(record: EcommerceHistoryRecord): EcommerceHistoryRecord {
return {
...record,
productImages: removeFilePayloadFromImages(record.productImages),
referenceImages: removeFilePayloadFromImages(record.referenceImages ?? []),
results: record.results ?? [],
setResultImages: record.setResultImages ?? [],
setCounts: record.setCounts ?? defaultCloneSetCounts,
detailModules: record.detailModules ?? defaultCloneDetailModuleIds,
modelScenes: record.modelScenes ?? [],
replicateLevel: record.replicateLevel ?? "high",
};
}
function readEcommerceHistoryRecords() {
if (typeof window === "undefined") return [];
try {
const rawValue = window.localStorage.getItem(ecommerceHistoryStorageKey);
if (!rawValue) return [];
const parsedValue: unknown = JSON.parse(rawValue);
if (!Array.isArray(parsedValue)) return [];
return parsedValue
.filter(isEcommerceHistoryRecord)
.map(normalizeEcommerceHistoryRecord)
.sort((a, b) => b.createdAt - a.createdAt)
.slice(0, 30);
} catch {
return [];
}
}
function writeEcommerceHistoryRecords(records: EcommerceHistoryRecord[]) {
if (typeof window === "undefined") return;
window.localStorage.setItem(ecommerceHistoryStorageKey, JSON.stringify(records.map(normalizeEcommerceHistoryRecord).slice(0, 30)));
}
function clampCloneVideoDuration(value: number) {
return Math.min(cloneVideoDurationMax, Math.max(cloneVideoDurationMin, Math.round(value)));
@@ -2680,7 +2492,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
}, [latestCloneSettingSnapshot]);
useEffect(() => {
window.localStorage.removeItem(cloneLatestSettingStorageKey);
clearCloneLatestSetting();
}, []);
useEffect(() => {
@@ -0,0 +1,212 @@
// 克隆 / 电商历史的本地持久化模块。
// 从 EcommercePage.tsx 抽出,逻辑零改动。
// 把 localStorage 读写 + 字段校验 + 默认值收口在此,页面只调用 read/write。
//
// 领域类型(CloneImageItem / CloneResult / CloneSavedSetting / EcommerceHistoryRecord
// 及其依赖的 type alias)也定义在此并 export,因为它们本质上是"持久化数据契约";
// EcommercePage 从这里 re-import,避免循环依赖(类型 import 编译期擦除)。
import type { CloneOutputKey } from "./platformRules";
export type CloneSetCountKey = "selling" | "white" | "scene";
export type CloneModelPanelTab = "scene" | "model";
export type CloneVideoQualityKey = "standard" | "high" | "ultra";
export type CloneReplicateLevelKey = "style" | "high";
export type CloneReferenceMode = "upload" | "link";
export interface CloneImageItem {
id: string;
src: string;
name: string;
file?: File;
width?: number;
height?: number;
format?: string;
mimeType?: string;
ossKey?: string;
}
export interface CloneResult {
id: string;
src: string;
label: string;
type?: "image" | "video";
}
export interface CloneSavedSetting {
id: string;
name: string;
savedAt: string;
output: CloneOutputKey;
platform: string;
market: string;
language: string;
ratio: string;
setCounts: Record<CloneSetCountKey, number>;
detailModules: string[];
modelPanelTab: CloneModelPanelTab;
modelScenes: string[];
modelCustomScene: string;
modelGender: string;
modelAge: string;
modelEthnicity: string;
modelBody: string;
modelAppearance: string;
videoQuality: CloneVideoQualityKey;
videoDurationSeconds: number;
videoSmart: boolean;
referenceMode?: CloneReferenceMode;
replicateLevel?: CloneReplicateLevelKey;
requirement: string;
}
export interface EcommerceHistoryRecord {
id: string;
title: string;
createdAt: number;
output: CloneOutputKey;
platform: string;
market: string;
language: string;
ratio: string;
requirement: string;
productImages: CloneImageItem[];
results: CloneResult[];
setResultImages: string[];
setCounts: Record<CloneSetCountKey, number>;
detailModules: string[];
modelScenes: string[];
referenceImages: CloneImageItem[];
replicateLevel: CloneReplicateLevelKey;
}
export const cloneLatestSettingStorageKey = "omniai.clone-ai.latest-setting";
export const ecommerceHistoryStorageKey = "omniai.ecommerce.history.records";
export const defaultCloneSetCounts: Record<CloneSetCountKey, number> = {
selling: 3,
white: 1,
scene: 3,
};
export const defaultCloneDetailModuleIds = ["hero", "selling", "usage", "angle", "scene", "detail"];
export function isCloneImageItem(item: unknown): item is CloneImageItem {
const candidate = item as Partial<CloneImageItem>;
return typeof candidate.id === "string" && typeof candidate.src === "string" && typeof candidate.name === "string";
}
export function isCloneResult(item: unknown): item is CloneResult {
const candidate = item as Partial<CloneResult>;
return typeof candidate.id === "string" && typeof candidate.src === "string" && typeof candidate.label === "string";
}
export function isEcommerceHistoryRecord(item: unknown): item is EcommerceHistoryRecord {
const candidate = item as Partial<EcommerceHistoryRecord>;
return (
typeof candidate.id === "string" &&
typeof candidate.title === "string" &&
typeof candidate.createdAt === "number" &&
typeof candidate.output === "string" &&
typeof candidate.platform === "string" &&
typeof candidate.market === "string" &&
typeof candidate.language === "string" &&
typeof candidate.ratio === "string" &&
typeof candidate.requirement === "string" &&
Array.isArray(candidate.productImages) &&
candidate.productImages.every(isCloneImageItem) &&
Array.isArray(candidate.results) &&
candidate.results.every(isCloneResult)
);
}
export function isCloneSavedSetting(item: unknown): item is CloneSavedSetting {
const candidate = item as Partial<CloneSavedSetting>;
return (
typeof candidate.id === "string" &&
typeof candidate.name === "string" &&
typeof candidate.savedAt === "string" &&
typeof candidate.output === "string" &&
typeof candidate.platform === "string" &&
typeof candidate.market === "string" &&
typeof candidate.language === "string" &&
typeof candidate.ratio === "string" &&
typeof candidate.videoDurationSeconds === "number"
);
}
export function removeFilePayloadFromImages(images: CloneImageItem[]): CloneImageItem[] {
return images.map(({ id, src, name, width, height, format, mimeType, ossKey }) => ({
id,
src,
name,
width,
height,
format,
mimeType,
ossKey,
}));
}
export function normalizeEcommerceHistoryRecord(record: EcommerceHistoryRecord): EcommerceHistoryRecord {
return {
...record,
productImages: removeFilePayloadFromImages(record.productImages),
referenceImages: removeFilePayloadFromImages(record.referenceImages ?? []),
results: record.results ?? [],
setResultImages: record.setResultImages ?? [],
setCounts: record.setCounts ?? defaultCloneSetCounts,
detailModules: record.detailModules ?? defaultCloneDetailModuleIds,
modelScenes: record.modelScenes ?? [],
replicateLevel: record.replicateLevel ?? "high",
};
}
export function readCloneLatestSetting(): CloneSavedSetting | null {
if (typeof window === "undefined") return null;
try {
const rawValue = window.localStorage.getItem(cloneLatestSettingStorageKey);
if (rawValue) {
const parsedValue: unknown = JSON.parse(rawValue);
if (isCloneSavedSetting(parsedValue)) return parsedValue;
}
} catch {
return null;
}
return null;
}
export function writeCloneLatestSetting(setting: CloneSavedSetting): void {
if (typeof window === "undefined") return;
window.localStorage.setItem(cloneLatestSettingStorageKey, JSON.stringify(setting));
}
export function clearCloneLatestSetting(): void {
if (typeof window === "undefined") return;
window.localStorage.removeItem(cloneLatestSettingStorageKey);
}
export function readEcommerceHistoryRecords(): EcommerceHistoryRecord[] {
if (typeof window === "undefined") return [];
try {
const rawValue = window.localStorage.getItem(ecommerceHistoryStorageKey);
if (!rawValue) return [];
const parsedValue: unknown = JSON.parse(rawValue);
if (!Array.isArray(parsedValue)) return [];
return parsedValue
.filter(isEcommerceHistoryRecord)
.map(normalizeEcommerceHistoryRecord)
.sort((a, b) => b.createdAt - a.createdAt)
.slice(0, 30);
} catch {
return [];
}
}
export function writeEcommerceHistoryRecords(records: EcommerceHistoryRecord[]): void {
if (typeof window === "undefined") return;
window.localStorage.setItem(
ecommerceHistoryStorageKey,
JSON.stringify(records.map(normalizeEcommerceHistoryRecord).slice(0, 30)),
);
}