2026-06-17 14:25:18 +08:00
|
|
|
|
import {
|
|
|
|
|
|
FileImageOutlined,
|
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
|
ThunderboltOutlined,
|
|
|
|
|
|
VideoCameraOutlined,
|
|
|
|
|
|
} from "@ant-design/icons";
|
2026-06-18 16:19:59 +08:00
|
|
|
|
import { useMemo, useRef, useState, type ChangeEvent, type DragEvent, type KeyboardEvent, type MouseEvent as ReactMouseEvent, type RefObject } from "react";
|
|
|
|
|
|
import { createPortal } from "react-dom";
|
2026-06-17 14:25:18 +08:00
|
|
|
|
import EcommerceVideoWorkspace from "../EcommerceVideoWorkspace";
|
|
|
|
|
|
|
|
|
|
|
|
interface CloneImageItem {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
src: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
file?: File;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type CloneVideoQualityKey = "standard" | "high" | "ultra";
|
|
|
|
|
|
|
|
|
|
|
|
interface EcommerceOneClickVideoPanelProps {
|
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
|
isAuthenticated: boolean;
|
|
|
|
|
|
onRequestLogin: () => void;
|
|
|
|
|
|
productImages: CloneImageItem[];
|
|
|
|
|
|
productInputRef: RefObject<HTMLInputElement>;
|
|
|
|
|
|
isProductUploadDragging: boolean;
|
|
|
|
|
|
setIsProductUploadDragging: (value: boolean) => void;
|
|
|
|
|
|
handleProductDrop: (event: DragEvent<HTMLDivElement>) => void;
|
|
|
|
|
|
handleProductUpload: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
|
|
|
|
removeProductImage: (imageId: string) => void;
|
|
|
|
|
|
maxProductImages: number;
|
|
|
|
|
|
requirement: string;
|
|
|
|
|
|
onRequirementChange: (value: string) => void;
|
|
|
|
|
|
platform: string;
|
|
|
|
|
|
platformOptions: string[];
|
|
|
|
|
|
onPlatformChange: (value: string) => void;
|
|
|
|
|
|
ratio: string;
|
|
|
|
|
|
ratioOptions: string[];
|
|
|
|
|
|
onRatioChange: (value: string) => void;
|
|
|
|
|
|
videoQuality: CloneVideoQualityKey;
|
|
|
|
|
|
videoQualityOptions: Array<{ key: CloneVideoQualityKey; label: string; desc: string }>;
|
|
|
|
|
|
onVideoQualityChange: (value: CloneVideoQualityKey) => void;
|
|
|
|
|
|
videoDuration: number;
|
|
|
|
|
|
videoDurationMin: number;
|
|
|
|
|
|
videoDurationMax: number;
|
|
|
|
|
|
onVideoDurationChange: (value: number) => void;
|
|
|
|
|
|
videoSmart: boolean;
|
|
|
|
|
|
onVideoSmartChange: (value: boolean) => void;
|
|
|
|
|
|
onOpenHistory: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getVideoAspectRatio(ratio: string): string {
|
|
|
|
|
|
if (ratio.includes("9:16")) return "9:16";
|
|
|
|
|
|
if (ratio.includes("16:9")) return "16:9";
|
|
|
|
|
|
if (ratio.includes("3:4")) return "3:4";
|
|
|
|
|
|
return "9:16";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openQuickUploadWithKeyboard(
|
|
|
|
|
|
event: KeyboardEvent<HTMLDivElement>,
|
|
|
|
|
|
inputRef: { current: HTMLInputElement | null },
|
|
|
|
|
|
) {
|
|
|
|
|
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
inputRef.current?.click();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function EcommerceOneClickVideoPanel({
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
isAuthenticated,
|
|
|
|
|
|
onRequestLogin,
|
|
|
|
|
|
productImages,
|
|
|
|
|
|
productInputRef,
|
|
|
|
|
|
isProductUploadDragging,
|
|
|
|
|
|
setIsProductUploadDragging,
|
|
|
|
|
|
handleProductDrop,
|
|
|
|
|
|
handleProductUpload,
|
|
|
|
|
|
removeProductImage,
|
|
|
|
|
|
maxProductImages,
|
|
|
|
|
|
requirement,
|
|
|
|
|
|
onRequirementChange,
|
|
|
|
|
|
platform,
|
|
|
|
|
|
platformOptions,
|
|
|
|
|
|
onPlatformChange,
|
|
|
|
|
|
ratio,
|
|
|
|
|
|
ratioOptions,
|
|
|
|
|
|
onRatioChange,
|
|
|
|
|
|
videoQuality,
|
|
|
|
|
|
videoQualityOptions,
|
|
|
|
|
|
onVideoQualityChange,
|
|
|
|
|
|
videoDuration,
|
|
|
|
|
|
videoDurationMin,
|
|
|
|
|
|
videoDurationMax,
|
|
|
|
|
|
onVideoDurationChange,
|
|
|
|
|
|
videoSmart,
|
|
|
|
|
|
onVideoSmartChange,
|
|
|
|
|
|
onOpenHistory,
|
|
|
|
|
|
}: EcommerceOneClickVideoPanelProps) {
|
|
|
|
|
|
const [openSelect, setOpenSelect] = useState<"platform" | "ratio" | null>(null);
|
|
|
|
|
|
const [planTrigger, setPlanTrigger] = useState(0);
|
2026-06-18 16:19:59 +08:00
|
|
|
|
const [hoverZoom, setHoverZoom] = useState<{ src: string; x: number; y: number; placement: "right" | "left" } | null>(null);
|
2026-06-17 14:28:45 +08:00
|
|
|
|
const selectAnchorRef = useRef<HTMLDivElement>(null);
|
2026-06-17 14:25:18 +08:00
|
|
|
|
|
|
|
|
|
|
const productImageDataUrls = useMemo(() => productImages.map((img) => img.src), [productImages]);
|
|
|
|
|
|
const productImageFiles = useMemo(() => productImages.map((img) => img.file), [productImages]);
|
|
|
|
|
|
|
|
|
|
|
|
const canGenerate = productImages.length > 0 || requirement.trim().length > 0;
|
|
|
|
|
|
|
|
|
|
|
|
const handleGenerate = () => {
|
|
|
|
|
|
if (!isAuthenticated) {
|
|
|
|
|
|
onRequestLogin();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setPlanTrigger((value) => value + 1);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handlePlatformSelect = (value: string) => {
|
|
|
|
|
|
onPlatformChange(value);
|
|
|
|
|
|
setOpenSelect(null);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleRatioSelect = (value: string) => {
|
|
|
|
|
|
onRatioChange(value);
|
|
|
|
|
|
setOpenSelect(null);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const toggleSelect = (key: "platform" | "ratio") => {
|
|
|
|
|
|
setOpenSelect((current) => (current === key ? null : key));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-18 16:19:59 +08:00
|
|
|
|
const handleThumbMouseEnter = (src: string, event: ReactMouseEvent<HTMLElement>) => {
|
|
|
|
|
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
|
|
|
|
const previewWidth = 300;
|
|
|
|
|
|
const previewHeight = 190;
|
|
|
|
|
|
const gap = 12;
|
|
|
|
|
|
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
|
|
|
|
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
|
|
|
|
const canShowRight = rect.right + gap + previewWidth <= viewportWidth - gap;
|
|
|
|
|
|
const placement: "right" | "left" = canShowRight ? "right" : "left";
|
|
|
|
|
|
const x = placement === "right" ? rect.right + gap : Math.max(gap, rect.left - gap);
|
|
|
|
|
|
const y = Math.min(
|
|
|
|
|
|
Math.max(rect.top + rect.height / 2, previewHeight / 2 + gap),
|
|
|
|
|
|
Math.max(previewHeight / 2 + gap, viewportHeight - previewHeight / 2 - gap),
|
|
|
|
|
|
);
|
|
|
|
|
|
setHoverZoom({ src, x, y, placement });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-17 14:25:18 +08:00
|
|
|
|
const renderThumbs = () => (
|
|
|
|
|
|
<div className="ecom-quick-upload-thumbs" aria-label="已上传商品原图">
|
|
|
|
|
|
{productImages.map((item) => (
|
2026-06-18 16:19:59 +08:00
|
|
|
|
<figure
|
|
|
|
|
|
key={item.id}
|
|
|
|
|
|
className="ecom-command-asset-thumb ecom-quick-upload-thumb"
|
|
|
|
|
|
onMouseEnter={(event) => handleThumbMouseEnter(item.src, event)}
|
|
|
|
|
|
onMouseLeave={() => setHoverZoom(null)}
|
|
|
|
|
|
>
|
2026-06-17 14:25:18 +08:00
|
|
|
|
<img src={item.src} alt={item.name} />
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-06-18 16:19:59 +08:00
|
|
|
|
className="ecom-hot-material-delete"
|
2026-06-17 14:25:18 +08:00
|
|
|
|
aria-label="删除图片"
|
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
|
event.stopPropagation();
|
2026-06-18 16:19:59 +08:00
|
|
|
|
setHoverZoom(null);
|
2026-06-17 14:25:18 +08:00
|
|
|
|
removeProductImage(item.id);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</figure>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<main className="ecom-one-click-video-page ecom-quick-hot-page ecom-quick-set-page ecom-tool-page-enter" aria-label="一键视频">
|
|
|
|
|
|
<div className="ecom-quick-set-body">
|
|
|
|
|
|
<aside className="ecom-quick-set-panel" aria-label="一键视频设置">
|
|
|
|
|
|
<header className="ecom-quick-set-panel-head">
|
|
|
|
|
|
<strong className="ecom-quick-set-page-title">
|
|
|
|
|
|
<VideoCameraOutlined /> 一键视频
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
<button type="button" className="ecom-quick-set-back" onClick={onClose}>
|
|
|
|
|
|
首页
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" className="ecom-quick-set-back" onClick={onClose}>
|
|
|
|
|
|
上一页
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
|
<strong><FileImageOutlined /> 上传商品原图</strong>
|
|
|
|
|
|
{productImages.length ? (
|
|
|
|
|
|
<div
|
|
|
|
|
|
role="button"
|
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
|
className={`ecom-quick-set-upload ecom-quick-hot-material has-images${isProductUploadDragging ? " is-dragging" : ""}`}
|
|
|
|
|
|
onClick={() => productInputRef.current?.click()}
|
|
|
|
|
|
onKeyDown={(event) => openQuickUploadWithKeyboard(event, productInputRef)}
|
|
|
|
|
|
onDragOver={(event) => {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
if (event.dataTransfer.types.includes("Files")) setIsProductUploadDragging(true);
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDragLeave={(event) => {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
if (event.currentTarget === event.target || !event.currentTarget.contains(event.relatedTarget as Node)) {
|
|
|
|
|
|
setIsProductUploadDragging(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDrop={(event) => {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
setIsProductUploadDragging(false);
|
|
|
|
|
|
handleProductDrop(event);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{renderThumbs()}
|
|
|
|
|
|
{productImages.length < maxProductImages ? (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="ecom-quick-hot-add-btn"
|
|
|
|
|
|
aria-label="添加更多素材"
|
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
productInputRef.current?.click();
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<PlusOutlined />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div
|
|
|
|
|
|
role="button"
|
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
|
className={`ecom-quick-set-upload ecom-quick-hot-material${isProductUploadDragging ? " is-dragging" : ""}`}
|
|
|
|
|
|
onClick={() => productInputRef.current?.click()}
|
|
|
|
|
|
onKeyDown={(event) => openQuickUploadWithKeyboard(event, productInputRef)}
|
|
|
|
|
|
onDragOver={(event) => {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
if (event.dataTransfer.types.includes("Files")) setIsProductUploadDragging(true);
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDragLeave={(event) => {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
if (event.currentTarget === event.target || !event.currentTarget.contains(event.relatedTarget as Node)) {
|
|
|
|
|
|
setIsProductUploadDragging(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDrop={(event) => {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
setIsProductUploadDragging(false);
|
|
|
|
|
|
handleProductDrop(event);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FileImageOutlined />
|
|
|
|
|
|
<span>拖拽或点击上传</span>
|
|
|
|
|
|
<em>上传商品素材图,最多 {maxProductImages} 张</em>
|
|
|
|
|
|
<b>+ 上传图片</b>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<input
|
|
|
|
|
|
ref={productInputRef}
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
accept="image/*"
|
|
|
|
|
|
multiple
|
|
|
|
|
|
className="ecom-command-hidden-file"
|
|
|
|
|
|
onChange={handleProductUpload}
|
|
|
|
|
|
aria-label="上传商品图片"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section className="ecom-quick-hot-requirement">
|
|
|
|
|
|
<div className="ecom-quick-hot-requirement__head">
|
|
|
|
|
|
<strong>视频需求</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="ecom-quick-hot-requirement__input">
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
value={requirement}
|
|
|
|
|
|
onChange={(event) => onRequirementChange(event.target.value.slice(0, 500))}
|
|
|
|
|
|
placeholder="建议包含以下信息:产品名称、核心卖点、期望场景、口播风格、具体参数"
|
|
|
|
|
|
maxLength={500}
|
|
|
|
|
|
rows={4}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span>{requirement.length}/500</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section className="ecom-quick-set-basic-section">
|
|
|
|
|
|
<span className="ecom-quick-set-label">基础设置</span>
|
|
|
|
|
|
<div className="ecom-quick-set-select-anchor" ref={selectAnchorRef}>
|
|
|
|
|
|
<div className="ecom-quick-set-selects">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className={openSelect === "platform" ? "is-active" : ""}
|
|
|
|
|
|
onClick={() => toggleSelect("platform")}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>平台</span>
|
|
|
|
|
|
<strong>{platform}</strong>
|
|
|
|
|
|
<em>⌄</em>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className={openSelect === "ratio" ? "is-active" : ""}
|
|
|
|
|
|
onClick={() => toggleSelect("ratio")}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>尺寸比例</span>
|
|
|
|
|
|
<strong>{ratio.replace(/\s+/g, " ").trim()}</strong>
|
|
|
|
|
|
<em>⌄</em>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{openSelect ? (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="ecom-quick-set-dropdown"
|
|
|
|
|
|
role="listbox"
|
|
|
|
|
|
aria-label={openSelect === "platform" ? "平台" : "尺寸比例"}
|
|
|
|
|
|
>
|
|
|
|
|
|
{(openSelect === "platform" ? platformOptions : ratioOptions).map((option) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={option}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className={
|
|
|
|
|
|
(openSelect === "platform" ? platform === option : ratio === option) ? "is-active" : ""
|
|
|
|
|
|
}
|
|
|
|
|
|
role="option"
|
|
|
|
|
|
aria-selected={openSelect === "platform" ? platform === option : ratio === option}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
if (openSelect === "platform") {
|
|
|
|
|
|
handlePlatformSelect(option);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
handleRatioSelect(option);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{option.replace(/\s+/g, " ").trim()}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
|
<strong>视频画质</strong>
|
|
|
|
|
|
<div className="ecom-quick-detail-modules">
|
|
|
|
|
|
{videoQualityOptions.map((option) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={option.key}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className={videoQuality === option.key ? "is-active" : ""}
|
|
|
|
|
|
aria-pressed={videoQuality === option.key}
|
|
|
|
|
|
onClick={() => onVideoQualityChange(option.key)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<strong>{option.label}</strong>
|
|
|
|
|
|
<span>{option.desc}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
|
<strong>视频时长</strong>
|
|
|
|
|
|
<div className="ecom-one-click-video-duration">
|
|
|
|
|
|
<span>{videoDuration} 秒</span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="range"
|
|
|
|
|
|
className="ecom-one-click-video-range"
|
|
|
|
|
|
min={videoDurationMin}
|
|
|
|
|
|
max={videoDurationMax}
|
|
|
|
|
|
step={5}
|
|
|
|
|
|
value={videoDuration}
|
|
|
|
|
|
onChange={(event) => onVideoDurationChange(Number(event.target.value))}
|
|
|
|
|
|
aria-label="视频时长"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="ecom-one-click-video-duration-scale" aria-hidden="true">
|
|
|
|
|
|
<span>{videoDurationMin}秒</span>
|
|
|
|
|
|
<span>{videoDurationMax}秒</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className={`ecom-one-click-video-smart${videoSmart ? " is-on" : ""}`}
|
|
|
|
|
|
aria-pressed={videoSmart}
|
|
|
|
|
|
onClick={() => onVideoSmartChange(!videoSmart)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>
|
|
|
|
|
|
<strong>智能优化</strong>
|
|
|
|
|
|
<em>根据平台、商品图和尺寸自动匹配推荐参数</em>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<i aria-hidden="true" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="ecom-quick-hot-actions">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="ecom-quick-set-primary ecom-one-click-video-generate"
|
|
|
|
|
|
onClick={handleGenerate}
|
|
|
|
|
|
disabled={!canGenerate}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ThunderboltOutlined /> {isAuthenticated ? "一键生成视频" : "登录后生成"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</aside>
|
2026-06-18 16:19:59 +08:00
|
|
|
|
{hoverZoom && typeof document !== "undefined"
|
|
|
|
|
|
? createPortal(
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`ecom-hot-material-zoom-portal is-${hoverZoom.placement}`}
|
|
|
|
|
|
style={{ left: hoverZoom.x, top: hoverZoom.y }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<img src={hoverZoom.src} alt="" />
|
|
|
|
|
|
</div>,
|
|
|
|
|
|
document.body,
|
|
|
|
|
|
)
|
|
|
|
|
|
: null}
|
2026-06-17 14:25:18 +08:00
|
|
|
|
|
|
|
|
|
|
<section className="ecom-quick-set-stage">
|
|
|
|
|
|
<EcommerceVideoWorkspace
|
|
|
|
|
|
isAuthenticated={isAuthenticated}
|
|
|
|
|
|
productImageDataUrls={productImageDataUrls}
|
|
|
|
|
|
productImageFiles={productImageFiles}
|
|
|
|
|
|
requirement={requirement}
|
|
|
|
|
|
platform={platform}
|
|
|
|
|
|
aspectRatio={getVideoAspectRatio(ratio)}
|
|
|
|
|
|
durationSeconds={videoDuration}
|
|
|
|
|
|
resolution={videoQuality === "standard" ? "720P" : "1080P"}
|
|
|
|
|
|
onRequestLogin={onRequestLogin}
|
|
|
|
|
|
onOpenHistory={onOpenHistory}
|
|
|
|
|
|
triggerPlan={planTrigger}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|