From 907e35cce8839181702fc0dc5063b4f1a218be56 Mon Sep 17 00:00:00 2001 From: Cho-P4 Date: Fri, 1 May 2026 18:24:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BA=E6=89=80=E6=9C=89=E5=BC=B9?= =?UTF-8?q?=E5=87=BA=E5=85=83=E7=B4=A0=E6=B7=BB=E5=8A=A0=E5=85=B3=E9=97=AD?= =?UTF-8?q?=E8=BF=87=E6=B8=A1=E5=8A=A8=E7=94=BB=20+=20=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E5=88=87=E6=8D=A2=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SearchBar:关闭时遮罩淡出 + 面板上滑收起 - 图片插入 Popover:关闭时缩放淡出 - 删除确认:window.confirm 替换为自定义毛玻璃模态框(含淡入/淡出) - 页面路由切换:离开时向上淡出,进入时向下淡入(含编辑器返回管理页) Co-Authored-By: Claude Sonnet 4.6 --- .claude/settings.local.json | 5 +- src/components/admin/MarkdownToolbar.jsx | 33 ++++++-- .../admin/MarkdownToolbar.module.css | 18 ++++- src/components/layout/AppShell.jsx | 32 ++++++-- src/components/layout/AppShell.module.css | 22 ++++++ src/components/ui/ConfirmModal.jsx | 57 ++++++++++++++ src/components/ui/ConfirmModal.module.css | 77 +++++++++++++++++++ src/components/ui/SearchBar.jsx | 38 +++++++-- src/components/ui/SearchBar.module.css | 22 +++++- src/pages/AdminPage.jsx | 20 +++-- 10 files changed, 293 insertions(+), 31 deletions(-) create mode 100644 src/components/layout/AppShell.module.css create mode 100644 src/components/ui/ConfirmModal.jsx create mode 100644 src/components/ui/ConfirmModal.module.css diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 735cc3c..e6cc6fb 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,7 +6,10 @@ "mcp__Claude_in_Chrome__tabs_context_mcp", "Bash(git init *)", "Bash(git add *)", - "Bash(git commit -m ' *)" + "Bash(git commit -m ' *)", + "Bash(git remote *)", + "Bash(git push *)", + "Bash(git commit -m 'docs: 重写 README 为中文,添加英文版跳转链接 *)" ] } } diff --git a/src/components/admin/MarkdownToolbar.jsx b/src/components/admin/MarkdownToolbar.jsx index 64ebac2..876e02d 100644 --- a/src/components/admin/MarkdownToolbar.jsx +++ b/src/components/admin/MarkdownToolbar.jsx @@ -1,4 +1,4 @@ -import { useState, useRef } from 'react'; +import { useState, useRef, useCallback } from 'react'; import styles from './MarkdownToolbar.module.css'; const TOOLS = [ @@ -9,13 +9,32 @@ const TOOLS = [ { label: '', title: '代码', wrap: (s) => `${s}` }, ]; +const CLOSE_DURATION = 160; + export default function MarkdownToolbar({ textareaRef }) { const [imgPopover, setImgPopover] = useState(false); + const [popoverClosing, setPopoverClosing] = useState(false); const [imgUrl, setImgUrl] = useState(''); const [imgAlt, setImgAlt] = useState(''); const [uploading, setUploading] = useState(false); const fileInputRef = useRef(null); + const closePopover = useCallback(() => { + setPopoverClosing(true); + setTimeout(() => { + setImgPopover(false); + setPopoverClosing(false); + }, CLOSE_DURATION); + }, []); + + const togglePopover = useCallback(() => { + if (imgPopover) { + closePopover(); + } else { + setImgPopover(true); + } + }, [imgPopover, closePopover]); + const insertAt = (html) => { const ta = textareaRef.current; if (!ta) return; @@ -52,7 +71,7 @@ export default function MarkdownToolbar({ textareaRef }) { insertAt(`${alt}`); setImgUrl(''); setImgAlt(''); - setImgPopover(false); + closePopover(); }; const handleFileChange = (e) => { @@ -69,7 +88,7 @@ export default function MarkdownToolbar({ textareaRef }) { const alt = file.name.replace(/\.[^.]+$/, '') || '图片'; insertAt(`${alt}`); setUploading(false); - setImgPopover(false); + closePopover(); e.target.value = ''; }; reader.readAsDataURL(file); @@ -94,16 +113,16 @@ export default function MarkdownToolbar({ textareaRef }) { {imgPopover && ( -
+

插入图片

@@ -162,7 +181,7 @@ export default function MarkdownToolbar({ textareaRef }) { diff --git a/src/components/admin/MarkdownToolbar.module.css b/src/components/admin/MarkdownToolbar.module.css index 25bbfcd..1037ea1 100644 --- a/src/components/admin/MarkdownToolbar.module.css +++ b/src/components/admin/MarkdownToolbar.module.css @@ -32,6 +32,12 @@ border-color: var(--color-muted); } +.toolActive { + background: var(--color-surface-2); + color: var(--color-ink); + border-color: var(--color-muted); +} + .separator { width: 1px; height: 20px; @@ -51,7 +57,17 @@ display: flex; flex-direction: column; gap: var(--space-4); - animation: slideDown 200ms cubic-bezier(0.34, 1.56, 0.64, 1); + animation: slideDown 200ms cubic-bezier(0.34, 1.56, 0.64, 1) both; + transform-origin: top left; +} + +.popoverOut { + animation: popoverCollapse 160ms cubic-bezier(0.4, 0, 1, 1) both; +} + +@keyframes popoverCollapse { + from { opacity: 1; transform: translateY(0) scale(1); } + to { opacity: 0; transform: translateY(-8px) scale(0.96); } } .popoverTitle { diff --git a/src/components/layout/AppShell.jsx b/src/components/layout/AppShell.jsx index 10a1671..0f67a3f 100644 --- a/src/components/layout/AppShell.jsx +++ b/src/components/layout/AppShell.jsx @@ -3,26 +3,46 @@ import { useState, useEffect, useRef } from 'react'; import Header from './Header'; import Footer from './Footer'; import PetalCanvas from '../ui/PetalCanvas'; +import styles from './AppShell.module.css'; export default function AppShell() { const location = useLocation(); const prevPath = useRef(location.pathname); const [petalTrigger, setPetalTrigger] = useState(0); + // Track location key to re-trigger enter animation on every navigation + const [displayKey, setDisplayKey] = useState(location.key); + const [exiting, setExiting] = useState(false); + const exitTimer = useRef(null); + useEffect(() => { - if (prevPath.current !== location.pathname) { - prevPath.current = location.pathname; - setPetalTrigger(t => t + 1); + if (prevPath.current === location.pathname) return; + prevPath.current = location.pathname; + setPetalTrigger(t => t + 1); + + // Play exit on old content, then swap to new + setExiting(true); + clearTimeout(exitTimer.current); + exitTimer.current = setTimeout(() => { window.scrollTo({ top: 0, behavior: 'instant' }); - } - }, [location.pathname]); + setDisplayKey(location.key); + setExiting(false); + }, 160); + + return () => clearTimeout(exitTimer.current); + }, [location.pathname, location.key]); return ( <>
- +
+ +