feat(core): Wave 1 part1 — 浮层进场动画/ESC + Toast 归正 + 账户表横线
S4 浮层: overlays.tsx 新增共享 useOverlayTransition(挂载→下一帧上 .show 触发 scale(.96→1)/translateX 进场过渡;关闭先撤 .show 播退场再卸载)+ 统一 ESC 关闭, 应用 TeamModal/ConfirmModal/SuccessModal/Drawer;EmptyPanel 补 .ic-empty 灰 icon S3 Toast: ToastLike 改用设计系统 .toast(右下/单橙 ic-t/shadow-floating/滑入), 不再随 success/error 变绿红边;auth login-toast → .toast S9 表格: account-page.css 删 tbody td border-bottom(对照 V1 account.html 确认应无行线) 验证: tsc 0 · 交互走查 toast出现+ESC关弹窗 · 14 路由回归 boot 0 报错 · 登录 643ms Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8a0ae6a19d
commit
74e3c818c5
@@ -1,6 +1,6 @@
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { CheckCircle2, Music, Shield, Trash2, X } from "lucide-react";
|
||||
import { CheckCircle2, Inbox, Music, Shield, Trash2, X } from "lucide-react";
|
||||
|
||||
// 浮层打开时锁住 body 滚动(否则滚轮会滚动遮罩后面的页面,体感"遮罩没盖住")。
|
||||
// 多浮层叠开用计数,最后一个关闭才解锁。
|
||||
@@ -17,6 +17,36 @@ export function useBodyScrollLock(active: boolean) {
|
||||
}, [active]);
|
||||
}
|
||||
|
||||
// 浮层进出场过渡 + ESC 统一接管。
|
||||
// 进场:挂载后「下一帧」再上 .show,让 CSS 的 scale(.96→1)/translateX 过渡真正跑起来
|
||||
// (此前直接挂 .show,首帧即终态 → 没有进场动画)。
|
||||
// 退场:先撤 .show 播退场过渡,等过渡时长后再卸载。
|
||||
// ESC:open 时监听 Escape → onClose(对齐 MediaLightbox 既有行为)。
|
||||
export function useOverlayTransition(open: boolean, onClose?: () => void, durationMs = 260) {
|
||||
const [mounted, setMounted] = useState(open);
|
||||
const [show, setShow] = useState(false);
|
||||
useEffect(() => {
|
||||
let raf1 = 0;
|
||||
let raf2 = 0;
|
||||
let timer = 0;
|
||||
if (open) {
|
||||
setMounted(true);
|
||||
raf1 = requestAnimationFrame(() => { raf2 = requestAnimationFrame(() => setShow(true)); });
|
||||
} else {
|
||||
setShow(false);
|
||||
timer = window.setTimeout(() => setMounted(false), durationMs);
|
||||
}
|
||||
return () => { cancelAnimationFrame(raf1); cancelAnimationFrame(raf2); window.clearTimeout(timer); };
|
||||
}, [open, durationMs]);
|
||||
useEffect(() => {
|
||||
if (!open || !onClose) return;
|
||||
const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") onClose(); };
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => document.removeEventListener("keydown", onKey);
|
||||
}, [open, onClose]);
|
||||
return { mounted, show };
|
||||
}
|
||||
|
||||
// 通用媒体预览灯箱:点击图片放大 / 点击视频弹窗播放 / 点击音频弹窗试听(复用 .np-lightbox 样式)
|
||||
// 背景点击 / Esc / 关闭键都可关闭;点媒体本身不关闭。
|
||||
export function MediaLightbox({ open, src, kind, name, close }: {
|
||||
@@ -80,10 +110,11 @@ export function TeamModal({ open, title, subtitle, icon, close, children, footer
|
||||
footer?: ReactNode;
|
||||
}) {
|
||||
useBodyScrollLock(open);
|
||||
if (!open) return null;
|
||||
const { mounted, show } = useOverlayTransition(open, close);
|
||||
if (!mounted) return null;
|
||||
// 挂到 body:脱离 .content(z-index:1)层叠上下文,遮罩才能盖住头部/侧栏
|
||||
return createPortal(
|
||||
<div className="modal-bg show" onClick={close}>
|
||||
<div className={`modal-bg${show ? " show" : ""}`} onClick={close}>
|
||||
<div className="modal invite-modal" onClick={(event) => event.stopPropagation()}>
|
||||
<span className="corner-tr">+</span><span className="corner-bl">+</span>
|
||||
<div className="modal-h"><div className="ic-m">{icon}</div><div className="ti">{title}<span>{subtitle}</span></div><button className="x modal-x" type="button" onClick={close} aria-label="关闭"><X size={14} /></button></div>
|
||||
@@ -106,10 +137,11 @@ export function ConfirmModal({ open, title, detail, confirmText, subtitle = "//
|
||||
onConfirm: () => void | Promise<unknown>;
|
||||
}) {
|
||||
useBodyScrollLock(open);
|
||||
if (!open) return null;
|
||||
const { mounted, show } = useOverlayTransition(open, onCancel);
|
||||
if (!mounted) return null;
|
||||
// 挂到 body:脱离 .content(z-index:1)层叠上下文,遮罩才能盖住头部/侧栏
|
||||
return createPortal(
|
||||
<div className="modal-bg show" onClick={onCancel}>
|
||||
<div className={`modal-bg${show ? " show" : ""}`} onClick={onCancel}>
|
||||
<div className="modal" onClick={(event) => event.stopPropagation()}>
|
||||
<span className="corner-tr">+</span><span className="corner-bl">+</span>
|
||||
<div className="modal-h"><div className="ic-m">{icon ?? <Shield size={16} />}</div><div className="ti">{title}<span>{subtitle}</span></div></div>
|
||||
@@ -130,9 +162,10 @@ export function SuccessModal({ open, title, detail, actions, close }: {
|
||||
close: () => void;
|
||||
}) {
|
||||
useBodyScrollLock(open);
|
||||
if (!open) return null;
|
||||
const { mounted, show } = useOverlayTransition(open, close);
|
||||
if (!mounted) return null;
|
||||
return createPortal(
|
||||
<div className="modal-bg show" onClick={close}>
|
||||
<div className={`modal-bg${show ? " show" : ""}`} onClick={close}>
|
||||
<div className="modal" onClick={(event) => event.stopPropagation()}>
|
||||
<span className="corner-tr">+</span><span className="corner-bl">+</span>
|
||||
<div className="modal-h"><div className="ic-m"><CheckCircle2 size={16} /></div><div className="ti">{title}<span>// SUCCESS</span></div><button className="x modal-x" type="button" onClick={close} aria-label="关闭"><X size={14} /></button></div>
|
||||
@@ -146,12 +179,13 @@ export function SuccessModal({ open, title, detail, actions, close }: {
|
||||
|
||||
export function Drawer({ title, open, close, children }: { title: string; open: boolean; close: () => void; children: ReactNode }) {
|
||||
useBodyScrollLock(open);
|
||||
if (!open) return null;
|
||||
const { mounted, show } = useOverlayTransition(open, close);
|
||||
if (!mounted) return null;
|
||||
// 挂到 body:脱离 .content(z-index:1)层叠上下文,遮罩才能盖住头部/侧栏
|
||||
return createPortal(
|
||||
<>
|
||||
<div className="drawer-bg show" onClick={close} />
|
||||
<aside className="drawer show">
|
||||
<div className={`drawer-bg${show ? " show" : ""}`} onClick={close} />
|
||||
<aside className={`drawer${show ? " show" : ""}`}>
|
||||
<div className="drawer-h"><h3>{title}</h3><button className="x" type="button" onClick={close} aria-label="关闭"><X size={14} /></button></div>
|
||||
<div className="drawer-b">{children}</div>
|
||||
</aside>
|
||||
@@ -161,5 +195,5 @@ export function Drawer({ title, open, close, children }: { title: string; open:
|
||||
}
|
||||
|
||||
export function EmptyPanel({ title, action, onAction }: { title: string; action: string; onAction: () => void }) {
|
||||
return <div className="empty-state show"><h3>{title}</h3><p>// 先创建资料后进入下一步</p><button className="btn btn-primary" type="button" onClick={onAction}>{action}</button></div>;
|
||||
return <div className="empty-state show"><span className="ic-empty"><Inbox size={22} strokeWidth={1.5} aria-hidden="true" /></span><h3>{title}</h3><p>// 先创建资料后进入下一步</p><button className="btn btn-primary" type="button" onClick={onAction}>{action}</button></div>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user