feat(ecommerce): 电商模板改为从服务端 API 加载

- 新增 ecommerceTemplateClient,通过应用 API 拉取模板清单(符合 AGENTS.md 数据走 API 规则)
- EcommercePage 接入远程模板,按 categorySlug 映射到场景,补充 mediaType/sourceAssets
- 移除硬编码 popularCommerceScenarioTemplates,改为远程模板为空时回退本地
- 补充 ecommerce-standalone.css 模板条样式
- .gitignore 忽略 ecommerce-template-manifest.* 运行时清单(属 API/OSS 数据,不入库)
This commit is contained in:
2026-06-18 16:20:33 +08:00
parent a2ccf290e5
commit ba885fd6ff
4 changed files with 361 additions and 35 deletions
+6
View File
@@ -16,3 +16,9 @@ tmp/
*.swo *.swo
coverage/ coverage/
屏幕截图 *.png 屏幕截图 *.png
# Ecommerce template manifests are runtime/API data, not source (see AGENTS.md rule 4)
ecommerce-template-manifest.local.json
ecommerce-template-manifest.local.md
ecommerce-template-manifest.oss.json
ecommerce-template-manifest.oss.md
+58
View File
@@ -0,0 +1,58 @@
import { serverRequest } from "./serverConnection";
export interface EcommerceTemplateAsset {
fileName?: string;
extension?: string;
sizeBytes?: number;
assetIndex?: number;
ossKey?: string;
url?: string;
}
export interface EcommerceTemplatePreview {
fileName?: string;
extension?: string;
sizeBytes?: number;
ossKey?: string;
url?: string;
}
export interface EcommerceTemplateManifestItem {
id: string;
category?: string;
categorySlug?: string;
templateName?: string;
templateSlug?: string;
preview?: EcommerceTemplatePreview;
prompt?: string;
assets?: EcommerceTemplateAsset[];
}
export interface EcommerceTemplateListResult {
version?: number;
ossPrefix?: string;
generatedAt?: string;
templates: EcommerceTemplateManifestItem[];
total: number;
}
export async function listEcommerceTemplates(category?: string): Promise<EcommerceTemplateListResult> {
const search = new URLSearchParams();
if (category) search.set("category", category);
const suffix = search.toString();
const response = await serverRequest<EcommerceTemplateListResult>(
`ai/ecommerce/templates${suffix ? `?${suffix}` : ""}`,
{
method: "GET",
maxRetries: 1,
fallbackMessage: "Failed to load ecommerce templates",
},
);
return {
...response,
templates: Array.isArray(response.templates) ? response.templates : [],
total: Number.isFinite(response.total) ? response.total : Array.isArray(response.templates) ? response.templates.length : 0,
};
}
+156 -32
View File
@@ -224,6 +224,7 @@ const buildInspirationPrompt = (title: string, meta: string): string => {
}; };
import { aiGenerationClient } from "../../api/aiGenerationClient"; import { aiGenerationClient } from "../../api/aiGenerationClient";
import { listEcommerceTemplates, type EcommerceTemplateManifestItem } from "../../api/ecommerceTemplateClient";
import { ServerRequestError } from "../../api/serverConnection"; import { ServerRequestError } from "../../api/serverConnection";
import { waitForTask } from "../../api/taskSubscription"; import { waitForTask } from "../../api/taskSubscription";
import { toast } from "../../components/toast/toastStore"; import { toast } from "../../components/toast/toastStore";
@@ -277,6 +278,13 @@ type CloneTemplateAsset = {
title: string; title: string;
prompt: string; prompt: string;
mediaUrl: string; mediaUrl: string;
mediaType?: "image" | "video";
sourceAssets?: Array<{
url: string;
name: string;
ossKey?: string;
mimeType?: string;
}>;
}; };
interface CommerceScenarioTemplate extends CloneTemplateAsset { interface CommerceScenarioTemplate extends CloneTemplateAsset {
scenario: Exclude<CommerceScenarioKey, "popular">; scenario: Exclude<CommerceScenarioKey, "popular">;
@@ -416,6 +424,56 @@ const commerceScenarioOutputMap: Record<Exclude<CommerceScenarioKey, "popular">,
salesVideo: "video", salesVideo: "video",
}; };
const ecommerceTemplateCategoryMap: Record<string, Exclude<CommerceScenarioKey, "popular">> = {
poster: "poster",
"main-image": "mainImage",
"scene-image": "scene",
"festival-image": "festival",
"model-image": "model",
"background-replace": "background",
retouch: "retouch",
"sales-video": "salesVideo",
};
const getTemplateMediaType = (template: EcommerceTemplateManifestItem): "image" | "video" => {
const extension = template.preview?.extension?.toLowerCase() || template.preview?.url?.split("?")[0].split(".").pop()?.toLowerCase() || "";
return extension.includes("mp4") || extension.includes("webm") || extension.includes("mov") ? "video" : "image";
};
const mapRemoteTemplateToScenarioTemplate = (template: EcommerceTemplateManifestItem): CommerceScenarioTemplate | null => {
const scenario = ecommerceTemplateCategoryMap[String(template.categorySlug || "").trim()];
const mediaUrl = template.preview?.url?.trim();
if (!scenario || !template.id || !mediaUrl) return null;
const title = template.templateName?.trim() || template.templateSlug?.trim() || template.id;
const prompt = template.prompt?.trim() || title;
const sourceAssets = (template.assets || [])
.filter((asset) => typeof asset.url === "string" && asset.url.trim())
.map((asset, index) => {
const url = asset.url!.trim();
const extension = asset.extension?.replace(/^\./, "") || url.split("?")[0].split(".").pop() || "png";
return {
url,
name: asset.fileName?.trim() || `${title}-素材${asset.assetIndex || index + 1}.${extension}`,
ossKey: asset.ossKey,
mimeType: extension.toLowerCase() === "jpg" || extension.toLowerCase() === "jpeg" ? "image/jpeg" : "image/png",
};
});
return {
id: template.id,
scenario,
output: commerceScenarioOutputMap[scenario],
title,
desc: template.category?.trim() || commerceScenarioOptions.find((option) => option.key === scenario)?.desc || "",
badge: template.category?.trim() || commerceScenarioOptions.find((option) => option.key === scenario)?.label || title,
prompt,
mediaUrl,
mediaType: getTemplateMediaType(template),
sourceAssets,
};
};
const defaultCommerceIntentFallback: CommerceDefaultIntent = { kind: "image", scenario: "mainImage" }; const defaultCommerceIntentFallback: CommerceDefaultIntent = { kind: "image", scenario: "mainImage" };
const normalizeDefaultCommerceIntent = (value: unknown): CommerceDefaultIntent => { const normalizeDefaultCommerceIntent = (value: unknown): CommerceDefaultIntent => {
@@ -795,10 +853,6 @@ const commerceScenarioTemplates: CommerceScenarioTemplate[] = [
mediaUrl: ossAssets.ecommerce.inspiration.nightLightUnboxingDouyin, mediaUrl: ossAssets.ecommerce.inspiration.nightLightUnboxingDouyin,
}, },
]; ];
const popularCommerceScenarioTemplates = commerceScenarioOptions
.filter((option): option is { key: Exclude<CommerceScenarioKey, "popular">; label: string; desc: string; icon: ReactNode } => option.key !== "popular")
.map((option) => commerceScenarioTemplates.find((template) => template.scenario === option.key))
.filter((template): template is CommerceScenarioTemplate => Boolean(template));
const cloneSetCountOptions: Array<{ const cloneSetCountOptions: Array<{
key: CloneSetCountKey; key: CloneSetCountKey;
title: string; title: string;
@@ -1181,6 +1235,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const [isProductUploadDragging, setIsProductUploadDragging] = useState(false); const [isProductUploadDragging, setIsProductUploadDragging] = useState(false);
const [activeCommerceScenario, setActiveCommerceScenario] = useState<CommerceScenarioKey | null>(null); const [activeCommerceScenario, setActiveCommerceScenario] = useState<CommerceScenarioKey | null>(null);
const [isCommerceScenarioMoreOpen, setIsCommerceScenarioMoreOpen] = useState(false); const [isCommerceScenarioMoreOpen, setIsCommerceScenarioMoreOpen] = useState(false);
const [remoteCommerceScenarioTemplates, setRemoteCommerceScenarioTemplates] = useState<CommerceScenarioTemplate[] | null>(null);
const [cloneOutput, setCloneOutput] = useState<CloneOutputKey>(defaultCloneOutput); const [cloneOutput, setCloneOutput] = useState<CloneOutputKey>(defaultCloneOutput);
const [isCloneTemplateStripVisible, setIsCloneTemplateStripVisible] = useState(false); const [isCloneTemplateStripVisible, setIsCloneTemplateStripVisible] = useState(false);
const [videoHistoryVisible, setVideoHistoryVisible] = useState(false); const [videoHistoryVisible, setVideoHistoryVisible] = useState(false);
@@ -1675,6 +1730,24 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const [hotStatus, setHotStatus] = useState<DetailStatus>("idle"); const [hotStatus, setHotStatus] = useState<DetailStatus>("idle");
const [hotResultUrl, setHotResultUrl] = useState<string | null>(null); const [hotResultUrl, setHotResultUrl] = useState<string | null>(null);
const [hotProgress, setHotProgress] = useState(0); const [hotProgress, setHotProgress] = useState(0);
useEffect(() => {
let cancelled = false;
listEcommerceTemplates()
.then((response) => {
if (cancelled) return;
const templates = response.templates
.map(mapRemoteTemplateToScenarioTemplate)
.filter((template): template is CommerceScenarioTemplate => Boolean(template));
setRemoteCommerceScenarioTemplates(templates.length ? templates : null);
})
.catch(() => {
if (!cancelled) setRemoteCommerceScenarioTemplates(null);
});
return () => {
cancelled = true;
};
}, []);
const productSetRatioOptions = useMemo( const productSetRatioOptions = useMemo(
() => getPlatformRatioOptions(productSetPlatform, productSetOutput), () => getPlatformRatioOptions(productSetPlatform, productSetOutput),
[productSetOutput, productSetPlatform], [productSetOutput, productSetPlatform],
@@ -1736,11 +1809,22 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
: commerceScenarioOptions.filter((option) => primaryCommerceScenarioKeys.includes(option.key)), : commerceScenarioOptions.filter((option) => primaryCommerceScenarioKeys.includes(option.key)),
[isCommerceScenarioMoreOpen], [isCommerceScenarioMoreOpen],
); );
const effectiveCommerceScenarioTemplates = remoteCommerceScenarioTemplates?.length
? remoteCommerceScenarioTemplates
: commerceScenarioTemplates;
const popularCommerceScenarioTemplates = useMemo(
() =>
commerceScenarioOptions
.filter((option): option is { key: Exclude<CommerceScenarioKey, "popular">; label: string; desc: string; icon: ReactNode } => option.key !== "popular")
.map((option) => effectiveCommerceScenarioTemplates.find((template) => template.scenario === option.key))
.filter((template): template is CommerceScenarioTemplate => Boolean(template)),
[effectiveCommerceScenarioTemplates],
);
const activeCommerceScenarioTemplates = activeCommerceScenario === null const activeCommerceScenarioTemplates = activeCommerceScenario === null
? [] ? []
: activeCommerceScenario === "popular" : activeCommerceScenario === "popular"
? popularCommerceScenarioTemplates ? popularCommerceScenarioTemplates
: commerceScenarioTemplates.filter((template) => template.scenario === activeCommerceScenario); : effectiveCommerceScenarioTemplates.filter((template) => template.scenario === activeCommerceScenario);
const shouldShowScenarioSettings = activeCommerceScenario !== null && scenarioSettingsKeys.includes(activeCommerceScenario); const shouldShowScenarioSettings = activeCommerceScenario !== null && scenarioSettingsKeys.includes(activeCommerceScenario);
useEffect(() => { useEffect(() => {
templateStripRef.current?.scrollTo({ left: 0, behavior: "auto" }); templateStripRef.current?.scrollTo({ left: 0, behavior: "auto" });
@@ -5509,7 +5593,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
} else if (composerAssetTab === "recipe") { } else if (composerAssetTab === "recipe") {
content = ( content = (
<div className="ecom-command-library-list"> <div className="ecom-command-library-list">
{commerceScenarioTemplates.slice(0, 4).map((template) => ( {effectiveCommerceScenarioTemplates.slice(0, 4).map((template) => (
<button key={template.id} type="button" onClick={() => { handleCloneTemplateCardClick(template); setComposerMenu(null); }}> <button key={template.id} type="button" onClick={() => { handleCloneTemplateCardClick(template); setComposerMenu(null); }}>
<strong>{template.title}</strong> <strong>{template.title}</strong>
<span>{template.badge} · {template.desc}</span> <span>{template.badge} · {template.desc}</span>
@@ -5755,6 +5839,26 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
setVideoPlanTrigger((value) => value + 1); setVideoPlanTrigger((value) => value + 1);
} }
const showDefaultRoutingGeneratingState = () => {
setComposerMenu(null);
setIsCommandComposerCompact(true);
imageAbortRef.current = { current: false };
lastFailedActionRef.current = null;
setGenerationProgress(2);
setResults([]);
setProductSetResultImages([]);
setPreviewZoom(1);
setPreviewOffset({ x: 0, y: 0 });
previewOffsetRef.current = { x: 0, y: 0 };
setStatus("generating");
};
const resetDefaultRoutingGeneratingState = () => {
setStatus("idle");
setGenerationProgress(0);
setIsCommandComposerCompact(false);
};
const handleCommandGenerate = async () => { const handleCommandGenerate = async () => {
if (cloneOutput === "video") { if (cloneOutput === "video") {
handleStartVideoPlan(); handleStartVideoPlan();
@@ -5762,7 +5866,12 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
} }
if (isDefaultCommandRouting) { if (isDefaultCommandRouting) {
if (!canPlanVideo) return; if (!canPlanVideo) return;
if ((appUsage?.balanceCents ?? 0) <= 0) {
toast.error("积分不足,请充值后继续");
return;
}
setIsDefaultIntentRouting(true); setIsDefaultIntentRouting(true);
showDefaultRoutingGeneratingState();
try { try {
const intent = await classifyDefaultCommerceIntent({ const intent = await classifyDefaultCommerceIntent({
prompt: requirement, prompt: requirement,
@@ -5772,14 +5881,20 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
platform, platform,
}); });
if (intent.kind === "video") { if (intent.kind === "video") {
resetDefaultRoutingGeneratingState();
handleCloneOutputChange("video");
handleStartVideoPlan(); handleStartVideoPlan();
return; return;
} }
if (!canGenerate) { if (!canGenerate) {
resetDefaultRoutingGeneratingState();
toast.info("请先上传商品图"); toast.info("请先上传商品图");
return; return;
} }
handleGenerate(intent); handleGenerate(intent);
} catch (error) {
resetDefaultRoutingGeneratingState();
toast.error(error instanceof Error ? error.message : "智能识别失败,请重试");
} finally { } finally {
setIsDefaultIntentRouting(false); setIsDefaultIntentRouting(false);
} }
@@ -5838,36 +5953,41 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
}); });
}; };
const addTemplateImageToComposer = async (card: CloneTemplateAsset) => { const addTemplateAssetsToComposer = (card: CloneTemplateAsset) => {
if (productImages.length >= maxCloneProductImages) { const sourceAssets = card.sourceAssets?.filter((asset) => asset.url.trim()) || [];
toast.info("模板图片已达上限"); if (!sourceAssets.length) return;
return;
}
try {
const stamp = Date.now(); const stamp = Date.now();
const uploaded = await aiGenerationClient.uploadAssetByUrl({ const nextImages: CloneImageItem[] = sourceAssets.map((asset, index) => ({
sourceUrl: card.mediaUrl, id: `template-${card.id}-${stamp}-${index}`,
name: `${card.id}-${stamp}`, src: asset.url,
scope: ecommerceOssScopes.productSource, name: asset.name || `${card.title}-素材${index + 1}`,
ossKey: asset.ossKey,
mimeType: asset.mimeType,
format: getRemoteImageFormat(asset.mimeType || "", asset.url),
}));
let insertedImages: CloneImageItem[] = [];
setProductImages((current) => {
const userImages = current.filter((image) => !image.id.startsWith("template-"));
const remainingSlots = maxCloneProductImages - userImages.length;
if (remainingSlots <= 0) {
toast.info("模板素材已达上限");
return userImages;
}
insertedImages = nextImages.slice(0, remainingSlots);
return [...userImages, ...insertedImages];
}); });
const nextImage: CloneImageItem = {
id: `template-${card.id}-${stamp}`, insertedImages.forEach((image) => {
src: uploaded.url || card.mediaUrl, void readImageDimensions(image.src)
name: card.title,
ossKey: uploaded.ossKey,
};
setProductImages((current) => [...current, nextImage].slice(0, maxCloneProductImages));
void readImageDimensions(nextImage.src)
.then(({ width, height }) => { .then(({ width, height }) => {
setProductImages((current) => setProductImages((current) =>
current.map((item) => (item.id === nextImage.id ? { ...item, width, height } : item)), current.map((item) => (item.id === image.id ? { ...item, width, height } : item)),
); );
}) })
.catch(() => undefined); .catch(() => undefined);
} catch { });
toast.error("模板图片导入失败");
}
}; };
const handleCloneTemplateCardClick = (card: CommerceScenarioTemplate) => { const handleCloneTemplateCardClick = (card: CommerceScenarioTemplate) => {
@@ -5875,7 +5995,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
if (card.output !== cloneOutput) handleCloneOutputChange(card.output); if (card.output !== cloneOutput) handleCloneOutputChange(card.output);
setIsCloneTemplateStripVisible(true); setIsCloneTemplateStripVisible(true);
setComposerMenu(null); setComposerMenu(null);
void addTemplateImageToComposer(card); addTemplateAssetsToComposer(card);
applyComposerPrompt(card.prompt); applyComposerPrompt(card.prompt);
}; };
@@ -6250,9 +6370,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
</button> </button>
</div> </div>
</div> </div>
<span className="ecom-command-scenario-scroll-hint" aria-hidden="true">
{isCommerceScenarioMoreOpen ? "左右滑动查看全部场景" : "点击更多查看全部场景"}
</span>
<div className="clone-ai-input-wrapper ecom-command-composer"> <div className="clone-ai-input-wrapper ecom-command-composer">
{productImages.length ? ( {productImages.length ? (
<div className="ecom-command-asset-popover" aria-label={`已上传素材,${productImages.length}/${maxCloneProductImages}`}> <div className="ecom-command-asset-popover" aria-label={`已上传素材,${productImages.length}/${maxCloneProductImages}`}>
@@ -6435,13 +6552,20 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
}} }}
> >
<span className="ecom-command-template-card__media" aria-hidden="true"> <span className="ecom-command-template-card__media" aria-hidden="true">
{card.mediaType === "video" ? (
<video src={card.mediaUrl} muted playsInline loop preload="metadata" />
) : (
<img src={card.mediaUrl} alt="" loading="lazy" /> <img src={card.mediaUrl} alt="" loading="lazy" />
)}
</span> </span>
<span className="ecom-command-template-card__body"> <span className="ecom-command-template-card__body">
<span className="ecom-command-template-card__badge">{card.badge}</span> <span className="ecom-command-template-card__badge">{card.badge}</span>
<strong>{card.title}</strong> <strong>{card.title}</strong>
<em>{card.desc}</em> <em>{card.desc}</em>
</span> </span>
<span className="ecom-command-template-card__prompt" aria-hidden="true">
{card.prompt}
</span>
</button> </button>
))} ))}
</section> </section>
+138
View File
@@ -18702,6 +18702,144 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
} }
} }
/* Keep template cards fully readable inside narrow command workspaces. */
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"]:not(.is-history-detail) .ecom-command-template-carousel .ecom-command-template-card {
position: relative !important;
flex: 0 0 min(100%, clamp(252px, 24vw, 328px)) !important;
grid-template-columns: 1fr !important;
grid-template-rows: auto minmax(0, 1fr) !important;
gap: 8px !important;
box-sizing: border-box !important;
overflow: hidden !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"]:not(.is-history-detail) .ecom-command-template-carousel .ecom-command-template-card__media {
width: 100% !important;
min-width: 0 !important;
height: auto !important;
aspect-ratio: 16 / 9 !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"]:not(.is-history-detail) .ecom-command-template-card__media video {
display: block !important;
width: 100% !important;
height: 100% !important;
object-fit: contain !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"]:not(.is-history-detail) .ecom-command-template-carousel .ecom-command-template-card__media img,
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"]:not(.is-history-detail) .ecom-command-template-carousel .ecom-command-template-card__media video {
object-fit: contain !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"]:not(.is-history-detail) .ecom-command-template-carousel .ecom-command-template-card:hover .ecom-command-template-card__media img {
transform: none !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"]:not(.is-history-detail) .ecom-command-template-card__body strong {
display: -webkit-box !important;
white-space: normal !important;
overflow-wrap: anywhere !important;
-webkit-line-clamp: 2 !important;
-webkit-box-orient: vertical !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"]:not(.is-history-detail) .ecom-command-template-card__prompt {
position: absolute !important;
right: 10px !important;
left: 10px !important;
top: 10px !important;
z-index: 3 !important;
display: -webkit-box !important;
max-height: 86px !important;
padding: 2px 4px !important;
overflow: hidden !important;
border: 0 !important;
border-radius: 0 !important;
background: transparent !important;
box-shadow: none !important;
color: rgba(16, 32, 44, 0.72) !important;
font-size: 12px !important;
font-weight: 650 !important;
line-height: 1.45 !important;
text-align: center !important;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.86) !important;
opacity: 0 !important;
pointer-events: none !important;
transform: translateY(-12px) scale(0.98) !important;
transition:
opacity 180ms ease,
transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1),
box-shadow 220ms ease !important;
-webkit-box-orient: vertical !important;
-webkit-line-clamp: 4 !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"]:not(.is-history-detail) .ecom-command-template-card:hover .ecom-command-template-card__prompt,
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"]:not(.is-history-detail) .ecom-command-template-card:focus-visible .ecom-command-template-card__prompt {
opacity: 1 !important;
transform: translateY(0) scale(1) !important;
}
@media (max-width: 640px) {
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"]:not(.is-history-detail) .ecom-command-template-carousel .ecom-command-template-card {
flex-basis: min(100%, 300px) !important;
grid-template-columns: 1fr !important;
}
}
/* Apply the same 16:9 preview treatment to the generated/history compact template rail. */
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-template-carousel .ecom-command-template-card {
aspect-ratio: 16 / 9 !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-template-carousel .ecom-command-template-card__media {
position: absolute !important;
inset: 0 !important;
width: 100% !important;
min-width: 0 !important;
height: 100% !important;
aspect-ratio: 16 / 9 !important;
border: 0 !important;
border-radius: inherit !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-template-carousel .ecom-command-template-card__media img,
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-template-carousel .ecom-command-template-card__media video {
width: 100% !important;
height: 100% !important;
object-fit: contain !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-template-carousel .ecom-command-template-card__body {
position: absolute !important;
right: 0 !important;
bottom: 0 !important;
left: 0 !important;
z-index: 2 !important;
display: grid !important;
gap: 2px !important;
padding: 18px 8px 8px !important;
background: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(246, 252, 254, 0.72)) !important;
text-align: center !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-template-carousel .ecom-command-template-card__badge,
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-template-carousel .ecom-command-template-card__body em {
display: none !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-template-carousel .ecom-command-template-card__body strong {
display: block !important;
overflow: hidden !important;
color: rgba(85, 111, 126, 0.74) !important;
font-size: 11px !important;
font-weight: 760 !important;
line-height: 1.2 !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
/* Restore the colorful scenario feedback while keeping the compact responsive layout. */ /* Restore the colorful scenario feedback while keeping the compact responsive layout. */
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"]:not(.is-history-detail) .ecom-command-scenario-shell .ecom-command-scenario-tabs button:has(.ecom-command-mode-icon--popular) { html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"]:not(.is-history-detail) .ecom-command-scenario-shell .ecom-command-scenario-tabs button:has(.ecom-command-mode-icon--popular) {
--mode-accent: #c04468 !important; --mode-accent: #c04468 !important;