213 lines
6.8 KiB
TypeScript
213 lines
6.8 KiB
TypeScript
// 克隆 / 电商历史的本地持久化模块。
|
|
// 从 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)),
|
|
);
|
|
}
|