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/App.tsx b/src/App.tsx index 4f20bfc..b3be210 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -357,7 +357,7 @@ function App() { canvasAutoOpenedRecentRef.current = false; setWorkspaceExpanded(false); if (options?.resetView) { - handleSetView("workbench"); + handleSetView("login"); } }, [clearSessionState, setProjects, setProjectsLoaded, setUsage, clearTasks, setRuntimeNotifications, setServerNotifications, setCanvasWorkflow, setCurrentCanvasProjectId, setWorkspaceExpanded, handleSetView]); @@ -492,7 +492,7 @@ function App() { if (nextSession) { setSession(nextSession); } else { - clearAuthenticatedState(); + clearAuthenticatedState({ resetView: true }); } } finally { checking = false; 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/community/CommunityPage.tsx b/src/features/community/CommunityPage.tsx index 2d7d219..0356e98 100644 --- a/src/features/community/CommunityPage.tsx +++ b/src/features/community/CommunityPage.tsx @@ -477,8 +477,9 @@ function CommunityPage({ projects, isAuthenticated, onStartCreate, onOpenProject
+ {detailStatus === "generating" ? : null}