mirror of
https://github.com/GSManagerXZ/GameServerManager.git
synced 2026-06-05 20:39:44 +08:00
增加弹窗动画
This commit is contained in:
@@ -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"
|
||||
>
|
||||
取消
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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实例
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? '编辑任务' : '新建任务'}
|
||||
|
||||
Reference in New Issue
Block a user