增加弹窗动画

This commit is contained in:
小朱
2025-07-10 17:27:21 +08:00
parent f2f1d15c28
commit 1b13783f48
6 changed files with 179 additions and 46 deletions

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, { useState, useEffect } from 'react'
import { X, AlertTriangle, Folder, Trash2 } from 'lucide-react'
interface ConfirmDeleteDialogProps {
@@ -17,26 +17,54 @@ export const ConfirmDeleteDialog: React.FC<ConfirmDeleteDialogProps> = ({
onCancel
}) => {
const [deleteDirectory, setDeleteDirectory] = React.useState(false)
const [isVisible, setIsVisible] = useState(false)
const [isAnimating, setIsAnimating] = useState(false)
if (!isOpen) return null
useEffect(() => {
if (isOpen) {
setIsVisible(true)
setTimeout(() => setIsAnimating(true), 10)
} else {
setIsAnimating(false)
setTimeout(() => setIsVisible(false), 200)
}
}, [isOpen])
const handleCancel = () => {
setIsAnimating(false)
setTimeout(() => {
onCancel()
}, 200)
}
const handleConfirm = () => {
onConfirm(deleteDirectory)
setIsAnimating(false)
setTimeout(() => {
onConfirm(deleteDirectory)
}, 200)
}
if (!isVisible) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* 背景遮罩 */}
<div
className="absolute inset-0 bg-black bg-opacity-50 transition-opacity"
onClick={onCancel}
className={`absolute inset-0 bg-black transition-opacity duration-200 ${
isAnimating ? 'bg-opacity-50' : 'bg-opacity-0'
}`}
onClick={handleCancel}
/>
{/* 对话框内容 */}
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full mx-4 p-6">
<div className={`relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full mx-4 p-6 transform transition-all duration-200 ${
isAnimating ? 'scale-100 opacity-100' : 'scale-95 opacity-0'
}`}>
{/* 关闭按钮 */}
<button
onClick={onCancel}
onClick={handleCancel}
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
<X className="w-5 h-5" />
@@ -98,7 +126,7 @@ export const ConfirmDeleteDialog: React.FC<ConfirmDeleteDialogProps> = ({
{/* 操作按钮 */}
<div className="flex justify-end space-x-3">
<button
onClick={onCancel}
onClick={handleCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors"
>

View File

@@ -1,9 +1,40 @@
import React from 'react'
import React, { useState, useEffect } from 'react'
import { useNotificationStore } from '@/stores/notificationStore'
import { X, CheckCircle, AlertCircle, AlertTriangle, Info } from 'lucide-react'
import { Notification } from '@/types'
const NotificationContainer: React.FC = () => {
const { notifications, removeNotification } = useNotificationStore()
const [animatingNotifications, setAnimatingNotifications] = useState<Set<string>>(new Set())
const [exitingNotifications, setExitingNotifications] = useState<Set<string>>(new Set())
// 处理新通知的进入动画
useEffect(() => {
const newNotifications = notifications.filter(n => !animatingNotifications.has(n.id))
if (newNotifications.length > 0) {
const newIds = new Set(animatingNotifications)
newNotifications.forEach(n => newIds.add(n.id))
setAnimatingNotifications(newIds)
}
}, [notifications, animatingNotifications])
// 处理通知移除的退出动画
const handleRemoveNotification = (id: string) => {
setExitingNotifications(prev => new Set([...prev, id]))
setTimeout(() => {
removeNotification(id)
setExitingNotifications(prev => {
const newSet = new Set(prev)
newSet.delete(id)
return newSet
})
setAnimatingNotifications(prev => {
const newSet = new Set(prev)
newSet.delete(id)
return newSet
})
}, 300) // 与CSS动画时间匹配
}
const getIcon = (type: string) => {
switch (type) {
@@ -39,35 +70,50 @@ const NotificationContainer: React.FC = () => {
return (
<div className="fixed top-4 right-4 z-50 space-y-2 max-w-sm">
{notifications.map((notification) => (
<div
key={notification.id}
className={`
${getBackgroundColor(notification.type)}
border rounded-lg p-4 shadow-lg backdrop-blur-sm
transform transition-all duration-300 ease-in-out
hover:scale-105
`}
>
<div className="flex items-start space-x-3">
{getIcon(notification.type)}
<div className="flex-1 min-w-0">
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
{notification.title}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1">
{notification.message}
</p>
{notifications.map((notification) => {
const isExiting = exitingNotifications.has(notification.id)
const isEntering = !animatingNotifications.has(notification.id)
return (
<div
key={notification.id}
className={`
${getBackgroundColor(notification.type)}
border rounded-lg p-4 shadow-lg backdrop-blur-sm
transform transition-all duration-300 ease-in-out
hover:scale-105
${
isExiting
? 'translate-x-full opacity-0 scale-95'
: isEntering
? 'translate-x-0 opacity-100 scale-100 animate-slide-in-right'
: 'translate-x-0 opacity-100 scale-100'
}
`}
style={{
animation: isEntering ? 'slideInRight 0.3s ease-out forwards' : undefined
}}
>
<div className="flex items-start space-x-3">
{getIcon(notification.type)}
<div className="flex-1 min-w-0">
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
{notification.title}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1">
{notification.message}
</p>
</div>
<button
onClick={() => handleRemoveNotification(notification.id)}
className="flex-shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
<button
onClick={() => removeNotification(notification.id)}
className="flex-shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
))}
)
})}
</div>
)
}

View File

@@ -264,6 +264,65 @@
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
}
/* 通知动画 */
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
.animate-slide-in-right {
animation: slideInRight 0.3s ease-out forwards;
}
.animate-slide-out-right {
animation: slideOutRight 0.3s ease-in forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes scaleIn {
from {
transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.animate-fade-in {
animation: fadeIn 0.2s ease-out forwards;
}
.animate-scale-in {
animation: scaleIn 0.3s ease-out forwards;
}
/* 游戏字体 - 使用系统字体 */
.font-game {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

View File

@@ -1264,8 +1264,8 @@ const GameDeploymentPage: React.FC = () => {
{/* 安装配置对话框 */}
{showInstallModal && selectedGame && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4">
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 animate-fade-in">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4 transform transition-all duration-300 animate-scale-in">
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{selectedGame.info.game_nameCN}
@@ -1404,8 +1404,8 @@ const GameDeploymentPage: React.FC = () => {
{/* 创建Minecraft实例对话框 */}
{showCreateInstanceModal && downloadResult && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4">
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 animate-fade-in">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4 transform transition-all duration-300 animate-scale-in">
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Minecraft实例

View File

@@ -742,8 +742,8 @@ const InstanceManagerPage: React.FC = () => {
{/* 创建/编辑实例模态框 */}
{showCreateModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4 max-h-[90vh] overflow-y-auto">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 animate-fade-in">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4 max-h-[90vh] overflow-y-auto transform transition-all duration-300 animate-scale-in">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
{editingInstance ? '编辑实例' : '创建实例'}
</h2>
@@ -855,8 +855,8 @@ const InstanceManagerPage: React.FC = () => {
{/* 安装实例模态框 */}
{showInstallModal && selectedMarketInstance && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 animate-fade-in">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4 transform transition-all duration-300 animate-scale-in">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
: {selectedMarketInstance.name}
</h2>

View File

@@ -390,8 +390,8 @@ const ScheduledTasksPage: React.FC = () => {
{/* 新建/编辑任务模态框 */}
{showModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
<div className="w-full max-w-md glass rounded-lg border border-white/20 dark:border-gray-700/30">
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 animate-fade-in">
<div className="w-full max-w-md glass rounded-lg border border-white/20 dark:border-gray-700/30 transform transition-all duration-300 animate-scale-in">
<div className="p-6">
<h2 className="text-xl font-bold text-black dark:text-white mb-4">
{editingTask ? '编辑任务' : '新建任务'}