mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 03:00:49 +08:00
feat(modal): add floating close button and collapse animatio
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import type { PropsWithChildren, ReactNode } from 'react';
|
import { useState, useEffect, useCallback, type PropsWithChildren, type ReactNode } from 'react';
|
||||||
import { IconX } from './icons';
|
import { IconX } from './icons';
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
@@ -9,23 +9,41 @@ interface ModalProps {
|
|||||||
width?: number | string;
|
width?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Modal({ open, title, onClose, footer, width = 520, children }: PropsWithChildren<ModalProps>) {
|
const CLOSE_ANIMATION_DURATION = 350;
|
||||||
if (!open) return null;
|
|
||||||
|
|
||||||
const handleMaskClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
export function Modal({ open, title, onClose, footer, width = 520, children }: PropsWithChildren<ModalProps>) {
|
||||||
if (event.target === event.currentTarget) {
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
onClose();
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setIsVisible(true);
|
||||||
|
setIsClosing(false);
|
||||||
}
|
}
|
||||||
};
|
}, [open]);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setIsClosing(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
setIsClosing(false);
|
||||||
|
onClose();
|
||||||
|
}, CLOSE_ANIMATION_DURATION);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
if (!open && !isVisible) return null;
|
||||||
|
|
||||||
|
const overlayClass = `modal-overlay ${isClosing ? 'modal-overlay-closing' : 'modal-overlay-entering'}`;
|
||||||
|
const modalClass = `modal ${isClosing ? 'modal-closing' : 'modal-entering'}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal-overlay" onClick={handleMaskClick}>
|
<div className={overlayClass}>
|
||||||
<div className="modal" style={{ width }} role="dialog" aria-modal="true">
|
<div className={modalClass} style={{ width }} role="dialog" aria-modal="true">
|
||||||
|
<button className="modal-close-floating" onClick={handleClose} aria-label="Close">
|
||||||
|
<IconX size={20} />
|
||||||
|
</button>
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<div className="modal-title">{title}</div>
|
<div className="modal-title">{title}</div>
|
||||||
<button className="modal-close" onClick={onClose} aria-label="Close">
|
|
||||||
<IconX size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-body">{children}</div>
|
<div className="modal-body">{children}</div>
|
||||||
{footer && <div className="modal-footer">{footer}</div>}
|
{footer && <div className="modal-footer">{footer}</div>}
|
||||||
|
|||||||
@@ -350,6 +350,32 @@ textarea {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: $z-modal;
|
z-index: $z-modal;
|
||||||
padding: $spacing-lg;
|
padding: $spacing-lg;
|
||||||
|
|
||||||
|
&.modal-overlay-entering {
|
||||||
|
animation: modal-overlay-fade-in 0.25s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.modal-overlay-closing {
|
||||||
|
animation: modal-overlay-fade-out 0.35s ease-in forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modal-overlay-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modal-overlay-fade-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
@@ -361,12 +387,77 @@ textarea {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
// 关闭按钮中心位置: right 12px + 16px = 28px, top 12px + 16px = 28px
|
||||||
|
transform-origin: calc(100% - 28px) 28px;
|
||||||
|
|
||||||
|
&.modal-entering {
|
||||||
|
animation: modal-scale-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.modal-closing {
|
||||||
|
animation: modal-collapse-to-close 0.35s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modal-scale-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.85) translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modal-collapse-to-close {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-floating {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: $radius-full;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: color 0.15s ease, background-color 0.15s ease, transform 0.15s ease;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
padding: $spacing-md $spacing-lg;
|
padding: $spacing-md $spacing-lg;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
|
||||||
@@ -375,30 +466,6 @@ textarea {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
padding: 0;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: $radius-md;
|
|
||||||
transition: color 0.15s ease, background-color 0.15s ease;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
|
|||||||
Reference in New Issue
Block a user