refactor(ecommerce): 抽出克隆/历史持久化模块(#2 续)

This commit is contained in:
2026-06-15 16:06:45 +08:00
parent 6dd292207f
commit 62fcf461b6
2 changed files with 215 additions and 191 deletions
@@ -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)),
);
}