refactor(ecommerce): 抽出克隆/历史持久化模块(#2 续)
This commit is contained in:
@@ -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)),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user