feat: enhance ecommerce UI with typewriter animation, icon buttons, responsive layout, and refined design tokens

- EcommercePage.tsx: Add typewriter animation (useTypewriter hook) for slogan text with blinking cursor; replace text-based add/delete buttons with Ant Design icons (CloudUploadOutlined, DeleteOutlined); make command history panel responsive to window width (auto-collapse ≤1180px); update button labels from generic "添加" to context-specific "上传商品图" / "上传素材"
- New useTypewriter.ts hook: Character-by-character typewriter animation with configurable speed and pause-before-loop, auto-resets on text change
- reset.css: Add comprehensive HTML/body typography baseline (font-size, text-rendering, font-smoothing, line-height); extend reset coverage to select/canvas/svg elements; add overflow-wrap:anywhere for text-bearing elements; add min-width:0 to prevent form element overflow
- primitives.css: Add reusable typography utility classes (ui-page-title, ui-section-title, ui-body-copy) with design token references
- tokens.css: Expand design token set for typography scales, font weights, and leading values
- ecommerce-standalone.css: Add 689 lines of standalone ecommerce page styles
- Page CSS (ecommerce, image-workbench, more-tools, more, script-tokens, script-tokens-v5, studio-layout): Enhanced visual styles and layout refinements across all pages
- app-shell.css: Shell-level layout and styling improvements
This commit is contained in:
2026-06-11 11:31:39 +08:00
parent c367198385
commit bbea5d1e58
14 changed files with 1963 additions and 15 deletions
+28 -8
View File
@@ -2,6 +2,7 @@
AppstoreOutlined,
CloudUploadOutlined,
CloseOutlined,
DeleteOutlined,
FileImageOutlined,
FolderOpenOutlined,
FrownOutlined,
@@ -16,6 +17,7 @@
TableOutlined,
} from "@ant-design/icons";
import { useEffect, useMemo, useRef, useState, type CSSProperties, type ChangeEvent, type DragEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
import { useTypewriter } from "../../hooks/useTypewriter";
import "../../styles/pages/ecommerce.css";
import "../../styles/pages/local-theme-parity.css";
import { ossAssets } from "../../data/ossAssets";
@@ -1187,7 +1189,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const [visibleComposerMenu, setVisibleComposerMenu] = useState<ComposerMenuKey | null>(null);
const [isComposerMenuClosing, setIsComposerMenuClosing] = useState(false);
const [composerPopoverLeft, setComposerPopoverLeft] = useState(0);
const [isCommandHistoryCollapsed, setIsCommandHistoryCollapsed] = useState(false);
const [isCommandHistoryCollapsed, setIsCommandHistoryCollapsed] = useState(() => (typeof window !== "undefined" ? window.innerWidth <= 1180 : false));
const [openCloneModelSelect, setOpenCloneModelSelect] = useState<CloneModelSelectKey | null>(null);
const [cloneModelSelectDropUp, setCloneModelSelectDropUp] = useState(false);
const [cloneReferenceMode, setCloneReferenceMode] = useState<CloneReferenceMode>("upload");
@@ -1223,6 +1225,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
offsetY: 0,
});
const [isCommandComposerCompact, setIsCommandComposerCompact] = useState(false);
const typewriterText = useTypewriter("万物皆可AI,广告素材一键生成");
useEffect(() => {
return () => {
@@ -1238,6 +1241,16 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
previewOffsetRef.current = previewOffset;
}, [previewOffset]);
useEffect(() => {
if (typeof window === "undefined") return undefined;
const syncHistoryPanel = () => {
setIsCommandHistoryCollapsed(window.innerWidth <= 1180);
};
syncHistoryPanel();
window.addEventListener("resize", syncHistoryPanel);
return () => window.removeEventListener("resize", syncHistoryPanel);
}, []);
const previewTransformStyle = useMemo<CSSProperties>(
() => ({
transform: `translate3d(${previewOffset.x}px, ${previewOffset.y}px, 0) scale(${previewZoom})`,
@@ -3729,7 +3742,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
if (isCommandComposerCompact) setIsCommandComposerCompact(false);
}}
>
<h1 className={`ecom-command-title${status === "done" ? " is-after-generate" : ""}`}>AI广</h1>
<h1 className={`ecom-command-title${status === "done" ? " is-after-generate" : ""}`}>
{typewriterText}
<span className="typewriter-cursor" aria-hidden="true">|</span>
</h1>
<input
ref={cloneReferenceInputRef}
type="file"
@@ -3772,8 +3788,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
if (files.length) addComposerAssets(files);
}}
>
<span aria-hidden="true">+</span>
<strong></strong>
<span aria-hidden="true"><CloudUploadOutlined /></span>
<strong></strong>
</button>
{productImages.length || videoOutfitVideoFile ? (
<div className="ecom-command-asset-popover" aria-label="宸蹭笂浼犵礌鏉?">
@@ -3783,7 +3799,9 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
<span className="ecom-command-asset-zoom" aria-hidden="true">
<img src={image.src} alt="" />
</span>
<button type="button" onClick={() => removeProductImage(image.id)} aria-label="删除图片">×</button>
<button type="button" onClick={() => removeProductImage(image.id)} aria-label="删除图片">
<DeleteOutlined />
</button>
</figure>
))}
{videoOutfitVideoFile && videoOutfitPreviewUrl ? (
@@ -3792,7 +3810,9 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
<span className="ecom-command-asset-zoom" aria-hidden="true">
<video src={videoOutfitPreviewUrl} muted playsInline />
</span>
<button type="button" onClick={() => setVideoOutfitVideoFile(null)} aria-label="删除视频">×</button>
<button type="button" onClick={() => setVideoOutfitVideoFile(null)} aria-label="删除视频">
<DeleteOutlined />
</button>
</figure>
) : null}
<button type="button" className="ecom-command-asset-add" onClick={() => productInputRef.current?.click()} aria-label="继续上传">+</button>
@@ -3836,8 +3856,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
if (files.length) addComposerAssets(files);
}}
>
<span aria-hidden="true">+</span>
<strong></strong>
<span aria-hidden="true"><CloudUploadOutlined /></span>
<strong></strong>
</button>
<button type="button" className={composerMenu === "mode" ? "is-active" : ""} onClick={(event) => toggleComposerMenu("mode", event)}>
<span className="ecom-command-option-icon" aria-hidden="true"><AppstoreOutlined /></span>