From 6b9953625e2a28d70979be35dcf4401b27d17117 Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Tue, 2 Jun 2026 18:31:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20UI=20interaction=20polish=20=E2=80=94?= =?UTF-8?q?=20exit=20animations,=20hover=20effects,=20directional=20transi?= =?UTF-8?q?tions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add AnimatedPanel component with CSS transition-based enter/exit for Profile popover and Notification panel (140ms scale+fade) - Add nav-activate-pulse animation for floating-nav active indicator (320ms glow) - Add tool-panel-fade-in crossfade when switching ecommerce tools - Add carousel-card-label slide-up-in 260ms on active carousel card - Add feature-visual img hover scale(1.03)+brightness, experience-route hover translateY(-2px) - Add community-case-card--mosaic hover scale(1.02)+shadow lift - Add directional PageTransition: forward→slideX(20px), backward→slideX(-20px) - Move vite proxy target from hardcoded IP to VITE_DEV_PROXY env variable - Add .env.example for developer onboarding Co-Authored-By: Claude Opus 4.7 --- .env.example | 8 ++ src/components/AnimatedPanel.tsx | 54 ++++++++++++++ src/components/AppShell.tsx | 22 ++++-- src/components/NotificationCenter.tsx | 7 +- src/components/PageTransition.tsx | 44 ++++++++++- src/features/ecommerce/EcommercePage.tsx | 3 +- src/features/home/HomePage.tsx | 1 + src/styles/components/legacy-components.css | 2 - src/styles/components/motion.css | 35 +++++++++ src/styles/components/page-transition.css | 53 +++++++++++++ src/styles/pages/home.css | 30 ++++++++ src/styles/pages/legacy-pages.css | 2 - src/styles/shell/app-shell.css | 9 +++ src/styles/themes/dark-green.css | 7 ++ vite.config.ts | 82 +++++++++++---------- 15 files changed, 304 insertions(+), 55 deletions(-) create mode 100644 .env.example create mode 100644 src/components/AnimatedPanel.tsx diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..44008b1 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Dev proxy target — the backend API server +VITE_DEV_PROXY=http://47.110.225.76:3600 + +# Key server URL for auth/profile endpoints +VITE_KEY_SERVER_URL= + +# Main API base URL (used when not served from omniai.net.cn) +VITE_API_BASE_URL= \ No newline at end of file diff --git a/src/components/AnimatedPanel.tsx b/src/components/AnimatedPanel.tsx new file mode 100644 index 0000000..53e0df8 --- /dev/null +++ b/src/components/AnimatedPanel.tsx @@ -0,0 +1,54 @@ +import { useEffect, useRef, useState, type ReactNode } from "react"; + +interface AnimatedPanelProps { + open: boolean; + children: ReactNode; + className?: string; + /** Duration in ms for the exit animation before unmounting. */ + exitDuration?: number; +} + +export function AnimatedPanel({ open, children, className, exitDuration = 140 }: AnimatedPanelProps) { + const [mounted, setMounted] = useState(open); + const [visible, setVisible] = useState(open); + const timerRef = useRef(null); + + useEffect(() => { + if (open) { + if (timerRef.current) { + window.clearTimeout(timerRef.current); + timerRef.current = null; + } + setMounted(true); + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setVisible(true); + }); + }); + } else { + setVisible(false); + timerRef.current = window.setTimeout(() => { + setMounted(false); + timerRef.current = null; + }, exitDuration); + } + }, [open, exitDuration]); + + useEffect(() => { + return () => { + if (timerRef.current) { + window.clearTimeout(timerRef.current); + } + }; + }, []); + + if (!mounted) return null; + + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index b1bdc16..6178646 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -16,6 +16,7 @@ import { canManageCommunityCases, canReviewCommunity } from "../features/communi import type { WebNavItem, WebNotification, WebUsageSummary, WebUserSession, WebViewKey } from "../types"; import NotificationCenter from "./NotificationCenter"; import { RechargeModal } from "./RechargeModal/RechargeModal"; +import { AnimatedPanel } from "./AnimatedPanel"; interface AppShellProps { activeView: WebViewKey; @@ -61,6 +62,8 @@ function AppShell({ const [profileOpen, setProfileOpen] = useState(false); const [rechargeOpen, setRechargeOpen] = useState(false); const [openSubmenuKey, setOpenSubmenuKey] = useState(null); + const prevActiveViewRef = useRef(activeView); + const [navJustActivated, setNavJustActivated] = useState(null); const isAuthView = activeView === "login"; const isImmersiveView = activeView === "agent" || activeView === "avatarConsole"; const showFloatingNav = !isAuthView && !isImmersiveView && activeView !== "home"; @@ -100,6 +103,15 @@ function AppShell({ [navItems], ); + useEffect(() => { + if (activeView !== prevActiveViewRef.current) { + setNavJustActivated(activeView); + prevActiveViewRef.current = activeView; + const timer = window.setTimeout(() => setNavJustActivated(null), 320); + return () => window.clearTimeout(timer); + } + }, [activeView]); + useEffect(() => { if (typeof document === "undefined") { return; @@ -223,8 +235,8 @@ function AppShell({ - {session && profileOpen ? ( -
+
{avatarUrl ? {displayName} : avatarLabel} @@ -410,8 +421,7 @@ function AppShell({ ) : null} -
- ) : null} +
diff --git a/src/components/NotificationCenter.tsx b/src/components/NotificationCenter.tsx index db9b9a0..586b9f2 100644 --- a/src/components/NotificationCenter.tsx +++ b/src/components/NotificationCenter.tsx @@ -10,6 +10,7 @@ import { } from "@ant-design/icons"; import { useEffect, useRef, useState } from "react"; import type { WebNotification, WebNotificationType, WebViewKey } from "../types"; +import { AnimatedPanel } from "./AnimatedPanel"; const NOTIFICATION_ICONS: Record = { task_completed: , @@ -115,8 +116,7 @@ function NotificationCenter({ items, onNavigate, onMarkRead, onMarkAllRead, onCl {unreadCount > 99 ? "99+" : unreadCount} )} - {open && ( -
+
通知中心
@@ -158,8 +158,7 @@ function NotificationCenter({ items, onNavigate, onMarkRead, onMarkAllRead, onCl )) )}
-
- )} +
); } diff --git a/src/components/PageTransition.tsx b/src/components/PageTransition.tsx index b008505..514b017 100644 --- a/src/components/PageTransition.tsx +++ b/src/components/PageTransition.tsx @@ -7,9 +7,40 @@ interface PageTransitionProps { const EXIT_DURATION_MS = 180; +const NAV_ORDER: string[] = [ + "home", + "workbench", + "ecommerce", + "ecommerceTemplates", + "sizeTemplate", + "canvas", + "scriptTokens", + "tokenUsage", + "community", + "assets", + "more", + "imageWorkbench", + "resolutionUpscale", + "watermarkRemoval", + "subtitleRemoval", + "digitalHuman", + "avatarConsole", + "characterMix", + "agent", + "settings", + "login", + "profile", + "report", +]; + +function getNavIndex(key: string): number { + return NAV_ORDER.indexOf(key); +} + export default function PageTransition({ viewKey, children }: PageTransitionProps) { const [displayedChildren, setDisplayedChildren] = useState(children); const [phase, setPhase] = useState<"idle" | "exit">("idle"); + const [direction, setDirection] = useState<"forward" | "backward" | "neutral">("neutral"); const prevKeyRef = useRef(viewKey); const timerRef = useRef>(); @@ -18,6 +49,15 @@ export default function PageTransition({ viewKey, children }: PageTransitionProp setDisplayedChildren(children); return; } + const prevIndex = getNavIndex(prevKeyRef.current); + const nextIndex = getNavIndex(viewKey); + if (prevIndex < nextIndex) { + setDirection("forward"); + } else if (prevIndex > nextIndex) { + setDirection("backward"); + } else { + setDirection("neutral"); + } prevKeyRef.current = viewKey; setPhase("exit"); timerRef.current = setTimeout(() => { @@ -27,8 +67,10 @@ export default function PageTransition({ viewKey, children }: PageTransitionProp return () => clearTimeout(timerRef.current); }, [viewKey, children]); + const dirClass = direction === "forward" ? " is-forward" : direction === "backward" ? " is-backward" : ""; + return ( -
+
{displayedChildren}
); diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index 6eb21c9..2750884 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -2819,7 +2819,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {