From 2759afa176530a6f6dbf578abdadd83dcbfe0a75 Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Wed, 17 Jun 2026 16:46:55 +0800 Subject: [PATCH] refactor: sync clonePersistence types, extract WatermarkToolPage --- src/features/ecommerce/EcommercePage.tsx | 373 ++---------------- .../panels/CommandHistorySidebar.tsx | 10 +- .../ecommerce/panels/WatermarkToolPage.tsx | 237 +++++++++++ .../ecommerce/utils/clonePersistence.ts | 98 ++++- 4 files changed, 362 insertions(+), 356 deletions(-) create mode 100644 src/features/ecommerce/panels/WatermarkToolPage.tsx diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index bd1e5e4..6016c3b 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -49,6 +49,7 @@ import EcommerceVideoHistoryPanel from "./panels/EcommerceVideoHistoryPanel"; import ProductSetHostingModal from "./panels/ProductSetHostingModal"; import ProductSetPreviewModal, { type ProductSetPreviewSelection } from "./panels/ProductSetPreviewModal"; import CommandHistorySidebar from "./panels/CommandHistorySidebar"; +import WatermarkToolPage from "./panels/WatermarkToolPage"; import EcommerceDetailPanel from "./panels/EcommerceDetailPanel"; import EcommerceSetPanel from "./panels/EcommerceSetPanel"; import EcommerceTryOnPanel from "./panels/EcommerceTryOnPanel"; @@ -58,16 +59,23 @@ import { ecommerceOssScopes, saveUnifiedEcommerceGenerationRecord, deleteEcommer import { downloadResultAsset } from "../workbench/workbenchDownload"; import type { CloneOutputKey, ProductSetOutputKey } from "./utils/platformRules"; import { + buildHistoryTurnFromRecord, cloneLatestSettingStorageKey, defaultCloneDetailModuleIds, defaultCloneSetCounts, ecommerceHistoryStorageKey, + getTurnResults, isCloneImageItem, isCloneResult, isCloneSavedSetting, + isEcommerceHistoryRecord, + normalizeEcommerceHistoryRecord, + normalizeEcommerceHistoryTurn, readCloneLatestSetting, + readEcommerceHistoryRecords, removeFilePayloadFromImages, writeCloneLatestSetting, + writeEcommerceHistoryRecords, } from "./utils/clonePersistence"; import type { CloneImageItem, @@ -78,6 +86,9 @@ import type { CloneSavedSetting, CloneSetCountKey, CloneVideoQualityKey, + EcommerceHistoryRecord, + EcommerceHistoryStatus, + EcommerceHistoryTurn, } from "./utils/clonePersistence"; const smartCutoutColorPresets = [ @@ -296,52 +307,6 @@ interface PreviewTouchGesture { startCenter: { x: number; y: number }; } -type EcommerceHistoryStatus = "generating" | "done" | "failed"; - -interface EcommerceHistoryTurn { - id: string; - createdAt: number; - status: EcommerceHistoryStatus; - errorMessage?: string; - output: CloneOutputKey; - platform: string; - market: string; - language: string; - ratio: string; - requirement: string; - productImages: CloneImageItem[]; - results: CloneResult[]; - setResultImages: string[]; - setCounts: Record; - detailModules: string[]; - modelScenes: string[]; - referenceImages: CloneImageItem[]; - replicateLevel: CloneReplicateLevelKey; -} - -interface EcommerceHistoryRecord { - id: string; - title: string; - createdAt: number; - status?: EcommerceHistoryStatus; - errorMessage?: string; - output: CloneOutputKey; - platform: string; - market: string; - language: string; - ratio: string; - requirement: string; - productImages: CloneImageItem[]; - results: CloneResult[]; - setResultImages: string[]; - setCounts: Record; - detailModules: string[]; - modelScenes: string[]; - referenceImages: CloneImageItem[]; - replicateLevel: CloneReplicateLevelKey; - turns?: EcommerceHistoryTurn[]; -} - interface EcommerceImagePromptOptions { gender?: string; age?: string; @@ -1436,127 +1401,6 @@ function notifyRejectedImages(files: File[]): File[] { return accepted; } -function isEcommerceHistoryRecord(item: unknown): item is EcommerceHistoryRecord { - const candidate = item as Partial; - 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 getTurnResults(turn: EcommerceHistoryTurn): CloneResult[] { - if (turn.results?.length) return turn.results.filter((item) => item.src); - if (turn.output !== "set") return []; - return (turn.setResultImages ?? []) - .filter(Boolean) - .map((src, index) => ({ id: `${turn.id}-set-${index}`, src, label: `套图 ${index + 1}` })); -} - -function buildHistoryTurnFromRecord(record: EcommerceHistoryRecord): EcommerceHistoryTurn { - return { - id: `${record.id}-turn-initial`, - createdAt: record.createdAt, - status: record.status ?? "done", - errorMessage: record.status === "failed" ? record.errorMessage : undefined, - output: record.output, - platform: record.platform, - market: record.market, - language: record.language, - ratio: record.ratio, - requirement: record.requirement, - productImages: record.productImages ?? [], - results: record.results ?? [], - setResultImages: record.setResultImages ?? [], - setCounts: record.setCounts ?? defaultCloneSetCounts, - detailModules: record.detailModules ?? defaultCloneDetailModuleIds, - modelScenes: record.modelScenes ?? [], - referenceImages: record.referenceImages ?? [], - replicateLevel: record.replicateLevel ?? "high", - }; -} - -function normalizeEcommerceHistoryTurn(turn: EcommerceHistoryTurn, fallback: EcommerceHistoryRecord, index: number): EcommerceHistoryTurn { - const status = turn.status ?? fallback.status ?? "done"; - return { - id: typeof turn.id === "string" && turn.id ? turn.id : `${fallback.id}-turn-${index + 1}`, - createdAt: typeof turn.createdAt === "number" ? turn.createdAt : fallback.createdAt, - status, - errorMessage: status === "failed" ? turn.errorMessage ?? fallback.errorMessage : undefined, - output: turn.output ?? fallback.output, - platform: turn.platform ?? fallback.platform, - market: turn.market ?? fallback.market, - language: turn.language ?? fallback.language, - ratio: turn.ratio ?? fallback.ratio, - requirement: turn.requirement ?? fallback.requirement, - productImages: removeFilePayloadFromImages(Array.isArray(turn.productImages) ? turn.productImages : fallback.productImages), - results: Array.isArray(turn.results) ? turn.results.filter(isCloneResult) : [], - setResultImages: Array.isArray(turn.setResultImages) ? turn.setResultImages.filter(Boolean) : [], - setCounts: turn.setCounts ?? fallback.setCounts ?? defaultCloneSetCounts, - detailModules: turn.detailModules ?? fallback.detailModules ?? defaultCloneDetailModuleIds, - modelScenes: turn.modelScenes ?? fallback.modelScenes ?? [], - referenceImages: removeFilePayloadFromImages(Array.isArray(turn.referenceImages) ? turn.referenceImages : fallback.referenceImages ?? []), - replicateLevel: turn.replicateLevel ?? fallback.replicateLevel ?? "high", - }; -} - -function normalizeEcommerceHistoryRecord(record: EcommerceHistoryRecord): EcommerceHistoryRecord { - const status = record.status ?? "done"; - const baseRecord = { - ...record, - status, - errorMessage: status === "failed" ? record.errorMessage : undefined, - 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", - }; - const rawTurns = Array.isArray(record.turns) && record.turns.length - ? record.turns - : [buildHistoryTurnFromRecord(baseRecord)]; - const turns = rawTurns.map((turn, index) => normalizeEcommerceHistoryTurn(turn, baseRecord, index)); - return { - ...baseRecord, - turns, - }; -} - -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))); } @@ -7112,184 +6956,23 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { ); const watermarkPreview = ( -
- - - -
- {!watermarkImage ? ( -
watermarkInputRef.current?.click()} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - watermarkInputRef.current?.click(); - } - }} - onDragEnter={(event) => { - event.preventDefault(); - setIsWatermarkDragging(true); - }} - onDragOver={(event) => event.preventDefault()} - onDragLeave={() => setIsWatermarkDragging(false)} - onDrop={handleWatermarkDrop} - > - - 点击或拖拽上传图片 - 支持 PNG / JPG / WebP,上传含水印图片后点击开始去水印 -
- ) : ( -
-
- 原图 - 原图 -
- -
- 去水印结果 - {watermarkStatus === "processing" ? ( -
- - 正在去水印 - AI 正在清理图片中的水印和文字 -
-
-
- {Math.round(watermarkProgress)}% -
- ) : watermarkStatus === "done" && watermarkResultUrl ? ( - <> - 去水印结果 - - - ) : watermarkStatus === "failed" ? ( -
- - 去水印失败 - 请检查网络或重试,如余额不足请先充值 -
- ) : ( -
- - 等待处理 - 点击开始去水印后显示结果 -
- )} -
- - -
-
-
- )} -
-
+ ); const translateLanguageOptions = [ diff --git a/src/features/ecommerce/panels/CommandHistorySidebar.tsx b/src/features/ecommerce/panels/CommandHistorySidebar.tsx index ffbd21b..03c341d 100644 --- a/src/features/ecommerce/panels/CommandHistorySidebar.tsx +++ b/src/features/ecommerce/panels/CommandHistorySidebar.tsx @@ -2,16 +2,10 @@ import { DeleteOutlined, MenuFoldOutlined, MenuUnfoldOutlined, ReloadOutlined } import type { MouseEvent as ReactMouseEvent } from "react"; import type { EcommerceHistoryRecord } from "../utils/clonePersistence"; -// 页面内的 EcommerceHistoryRecord 比 clonePersistence 版本更丰富(多了 status/errorMessage/turns), -// 这里用 intersection 补齐侧栏实际用到的字段,避免依赖页面的内部类型。 -type HistoryRecord = EcommerceHistoryRecord & { - status?: "generating" | "done" | "failed"; -}; - interface CommandHistorySidebarProps { collapsed: boolean; showBackdrop: boolean; - records: HistoryRecord[]; + records: EcommerceHistoryRecord[]; activeRecordId: string | null; isRefreshing: boolean; refreshMessage: string | null; @@ -23,7 +17,7 @@ interface CommandHistorySidebarProps { onCollapse: () => void; onNewConversation: () => void; onRefresh: () => void; - onOpenRecord: (record: HistoryRecord) => void; + onOpenRecord: (record: EcommerceHistoryRecord) => void; onDeleteRecord: (recordId: string, event: ReactMouseEvent) => void; } diff --git a/src/features/ecommerce/panels/WatermarkToolPage.tsx b/src/features/ecommerce/panels/WatermarkToolPage.tsx new file mode 100644 index 0000000..b21e4af --- /dev/null +++ b/src/features/ecommerce/panels/WatermarkToolPage.tsx @@ -0,0 +1,237 @@ +import { + CloudUploadOutlined, + FileImageOutlined, + FolderOpenOutlined, + FrownOutlined, + LoadingOutlined, + QuestionCircleOutlined, +} from "@ant-design/icons"; +import type { ChangeEvent, DragEvent, KeyboardEvent, RefObject } from "react"; +import { toast } from "../../../components/toast/toastStore"; + +export interface WatermarkImageItem { + src: string; + name: string; + format: string; +} + +export type WatermarkStatus = "idle" | "processing" | "done" | "failed"; + +interface WatermarkToolPageProps { + inputRef: RefObject; + urlInputRef: RefObject; + image: WatermarkImageItem | null; + isDragging: boolean; + status: WatermarkStatus; + progress: number; + resultUrl: string | null; + onUpload: (event: ChangeEvent) => void; + onDrop: (event: DragEvent) => void; + onDraggingChange: (dragging: boolean) => void; + onRemoveImage: () => void; + onUrlImport: () => void; + onGenerate: () => void; + onDownload: () => void; + onClose: () => void; +} + +// 去水印工具页面:上传含水印图片 → AI 清理 → 预览/下载结果。 +// 从 EcommercePage 的 watermarkPreview 抽出,状态与处理逻辑仍在父组件,本组件纯展示 + 回调。 +export default function WatermarkToolPage({ + inputRef, + urlInputRef, + image, + isDragging, + status, + progress, + resultUrl, + onUpload, + onDrop, + onDraggingChange, + onRemoveImage, + onUrlImport, + onGenerate, + onDownload, + onClose, +}: WatermarkToolPageProps) { + return ( +
+ + + +
+ {!image ? ( +
inputRef.current?.click()} + onKeyDown={(event: KeyboardEvent) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + inputRef.current?.click(); + } + }} + onDragEnter={(event) => { + event.preventDefault(); + onDraggingChange(true); + }} + onDragOver={(event) => event.preventDefault()} + onDragLeave={() => onDraggingChange(false)} + onDrop={onDrop} + > + + 点击或拖拽上传图片 + 支持 PNG / JPG / WebP,上传含水印图片后点击开始去水印 +
+ ) : ( +
+
+ 原图 + 原图 +
+ +
+ 去水印结果 + {status === "processing" ? ( +
+ + 正在去水印 + AI 正在清理图片中的水印和文字 +
+
+
+ {Math.round(progress)}% +
+ ) : status === "done" && resultUrl ? ( + <> + 去水印结果 + + + ) : status === "failed" ? ( +
+ + 去水印失败 + 请检查网络或重试,如余额不足请先充值 +
+ ) : ( +
+ + 等待处理 + 点击开始去水印后显示结果 +
+ )} +
+ + +
+
+
+ )} +
+
+ ); +} diff --git a/src/features/ecommerce/utils/clonePersistence.ts b/src/features/ecommerce/utils/clonePersistence.ts index 0c55638..4f5f505 100644 --- a/src/features/ecommerce/utils/clonePersistence.ts +++ b/src/features/ecommerce/utils/clonePersistence.ts @@ -60,10 +60,13 @@ export interface CloneSavedSetting { requirement: string; } -export interface EcommerceHistoryRecord { +export type EcommerceHistoryStatus = "generating" | "done" | "failed"; + +export interface EcommerceHistoryTurn { id: string; - title: string; createdAt: number; + status: EcommerceHistoryStatus; + errorMessage?: string; output: CloneOutputKey; platform: string; market: string; @@ -80,6 +83,29 @@ export interface EcommerceHistoryRecord { replicateLevel: CloneReplicateLevelKey; } +export interface EcommerceHistoryRecord { + id: string; + title: string; + createdAt: number; + status?: EcommerceHistoryStatus; + errorMessage?: string; + output: CloneOutputKey; + platform: string; + market: string; + language: string; + ratio: string; + requirement: string; + productImages: CloneImageItem[]; + results: CloneResult[]; + setResultImages: string[]; + setCounts: Record; + detailModules: string[]; + modelScenes: string[]; + referenceImages: CloneImageItem[]; + replicateLevel: CloneReplicateLevelKey; + turns?: EcommerceHistoryTurn[]; +} + export const cloneLatestSettingStorageKey = "omniai.clone-ai.latest-setting"; export const ecommerceHistoryStorageKey = "omniai.ecommerce.history.records"; @@ -148,9 +174,67 @@ export function removeFilePayloadFromImages(images: CloneImageItem[]): CloneImag })); } -export function normalizeEcommerceHistoryRecord(record: EcommerceHistoryRecord): EcommerceHistoryRecord { +export function getTurnResults(turn: EcommerceHistoryTurn): CloneResult[] { + if (turn.results?.length) return turn.results.filter((item) => item.src); + if (turn.output !== "set") return []; + return (turn.setResultImages ?? []) + .filter(Boolean) + .map((src, index) => ({ id: `${turn.id}-set-${index}`, src, label: `套图 ${index + 1}` })); +} + +export function buildHistoryTurnFromRecord(record: EcommerceHistoryRecord): EcommerceHistoryTurn { return { + id: `${record.id}-turn-initial`, + createdAt: record.createdAt, + status: record.status ?? "done", + errorMessage: record.status === "failed" ? record.errorMessage : undefined, + output: record.output, + platform: record.platform, + market: record.market, + language: record.language, + ratio: record.ratio, + requirement: record.requirement, + productImages: record.productImages ?? [], + results: record.results ?? [], + setResultImages: record.setResultImages ?? [], + setCounts: record.setCounts ?? defaultCloneSetCounts, + detailModules: record.detailModules ?? defaultCloneDetailModuleIds, + modelScenes: record.modelScenes ?? [], + referenceImages: record.referenceImages ?? [], + replicateLevel: record.replicateLevel ?? "high", + }; +} + +export function normalizeEcommerceHistoryTurn(turn: EcommerceHistoryTurn, fallback: EcommerceHistoryRecord, index: number): EcommerceHistoryTurn { + const status = turn.status ?? fallback.status ?? "done"; + return { + id: typeof turn.id === "string" && turn.id ? turn.id : `${fallback.id}-turn-${index + 1}`, + createdAt: typeof turn.createdAt === "number" ? turn.createdAt : fallback.createdAt, + status, + errorMessage: status === "failed" ? turn.errorMessage ?? fallback.errorMessage : undefined, + output: turn.output ?? fallback.output, + platform: turn.platform ?? fallback.platform, + market: turn.market ?? fallback.market, + language: turn.language ?? fallback.language, + ratio: turn.ratio ?? fallback.ratio, + requirement: turn.requirement ?? fallback.requirement, + productImages: removeFilePayloadFromImages(Array.isArray(turn.productImages) ? turn.productImages : fallback.productImages), + results: Array.isArray(turn.results) ? turn.results.filter(isCloneResult) : [], + setResultImages: Array.isArray(turn.setResultImages) ? turn.setResultImages.filter(Boolean) : [], + setCounts: turn.setCounts ?? fallback.setCounts ?? defaultCloneSetCounts, + detailModules: turn.detailModules ?? fallback.detailModules ?? defaultCloneDetailModuleIds, + modelScenes: turn.modelScenes ?? fallback.modelScenes ?? [], + referenceImages: removeFilePayloadFromImages(Array.isArray(turn.referenceImages) ? turn.referenceImages : fallback.referenceImages ?? []), + replicateLevel: turn.replicateLevel ?? fallback.replicateLevel ?? "high", + }; +} + +export function normalizeEcommerceHistoryRecord(record: EcommerceHistoryRecord): EcommerceHistoryRecord { + const status = record.status ?? "done"; + const baseRecord = { ...record, + status, + errorMessage: status === "failed" ? record.errorMessage : undefined, productImages: removeFilePayloadFromImages(record.productImages), referenceImages: removeFilePayloadFromImages(record.referenceImages ?? []), results: record.results ?? [], @@ -160,6 +244,14 @@ export function normalizeEcommerceHistoryRecord(record: EcommerceHistoryRecord): modelScenes: record.modelScenes ?? [], replicateLevel: record.replicateLevel ?? "high", }; + const rawTurns = Array.isArray(record.turns) && record.turns.length + ? record.turns + : [buildHistoryTurnFromRecord(baseRecord)]; + const turns = rawTurns.map((turn, index) => normalizeEcommerceHistoryTurn(turn, baseRecord, index)); + return { + ...baseRecord, + turns, + }; } export function readCloneLatestSetting(): CloneSavedSetting | null {