Skip to content

Commit 75f822a

Browse files
committed
component(form): 调整form
1 parent e9a5a7c commit 75f822a

10 files changed

Lines changed: 753 additions & 393 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
## TODO 列表:
77

88
- [x] 列表页增加布局切换(每行 1/2/4 列,可记忆用户选择)
9+
- [x] 收集报名表单组件调整。
910
- [ ] 优化文章封面展示(统一比例、兜底图、列表/详情图策略一致)
1011
- [ ] 优化文章代码块主题(语法高亮 + 深浅色方案)
1112
- [ ] 全站文案与间距节奏优化(标题、副标题、按钮、空态)

src/components/common/Modal.astro

Lines changed: 0 additions & 67 deletions
This file was deleted.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { useEffect, useState, type ReactNode } from "react";
2+
import { createPortal } from "react-dom";
3+
4+
interface ModalDialogProps {
5+
open: boolean;
6+
onClose: () => void;
7+
title?: ReactNode;
8+
children: ReactNode;
9+
footer?: ReactNode;
10+
showCloseButton?: boolean;
11+
closeOnEsc?: boolean;
12+
closeOnOverlay?: boolean;
13+
maxWidthClassName?: string;
14+
overlayClassName?: string;
15+
panelClassName?: string;
16+
headerClassName?: string;
17+
bodyClassName?: string;
18+
footerClassName?: string;
19+
}
20+
21+
export default function ModalDialog({
22+
open,
23+
onClose,
24+
title,
25+
children,
26+
footer,
27+
showCloseButton = true,
28+
closeOnEsc = true,
29+
closeOnOverlay = true,
30+
maxWidthClassName = "max-w-md",
31+
overlayClassName = "bg-black/50",
32+
panelClassName = "bg-white rounded-2xl shadow-2xl",
33+
headerClassName = "border-b border-brand-100 px-4 py-3",
34+
bodyClassName = "p-4",
35+
footerClassName = "border-t border-brand-100 px-4 py-3",
36+
}: ModalDialogProps) {
37+
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(
38+
null,
39+
);
40+
41+
useEffect(() => {
42+
setPortalContainer(document.body);
43+
}, []);
44+
45+
useEffect(() => {
46+
if (!open || !closeOnEsc) return;
47+
48+
const onKeyDown = (event: KeyboardEvent) => {
49+
if (event.key === "Escape") onClose();
50+
};
51+
52+
window.addEventListener("keydown", onKeyDown);
53+
return () => {
54+
window.removeEventListener("keydown", onKeyDown);
55+
};
56+
}, [closeOnEsc, onClose, open]);
57+
58+
useEffect(() => {
59+
if (!open) return;
60+
61+
const lockCount = Number(document.body.dataset.modalLockCount ?? "0") + 1;
62+
document.body.dataset.modalLockCount = String(lockCount);
63+
64+
const originalOverflow = document.body.style.overflow;
65+
document.body.style.overflow = "hidden";
66+
67+
return () => {
68+
const nextLockCount = Math.max(
69+
0,
70+
Number(document.body.dataset.modalLockCount ?? "1") - 1,
71+
);
72+
73+
if (nextLockCount === 0) {
74+
delete document.body.dataset.modalLockCount;
75+
document.body.style.overflow = originalOverflow;
76+
} else {
77+
document.body.dataset.modalLockCount = String(nextLockCount);
78+
}
79+
};
80+
}, [open]);
81+
82+
if (!open || !portalContainer) return null;
83+
84+
const modalNode = (
85+
<div
86+
className={`fixed inset-0 z-50 flex items-center justify-center p-4 ${overlayClassName}`}
87+
onClick={closeOnOverlay ? onClose : undefined}
88+
role="presentation"
89+
>
90+
<div
91+
className={`relative w-full overflow-hidden ${maxWidthClassName} ${panelClassName}`}
92+
onClick={(event) => event.stopPropagation()}
93+
role="dialog"
94+
aria-modal="true"
95+
aria-label={typeof title === "string" ? title : undefined}
96+
>
97+
{(title || showCloseButton) && (
98+
<div
99+
className={`flex items-center justify-between ${headerClassName}`}
100+
>
101+
<div className="text-sm font-semibold text-ink-900">{title}</div>
102+
{showCloseButton && (
103+
<button
104+
className="rounded-lg p-1.5 text-ink-500 transition hover:bg-brand-50 hover:text-ink-900"
105+
aria-label="关闭"
106+
type="button"
107+
onClick={onClose}
108+
>
109+
<svg
110+
xmlns="http://www.w3.org/2000/svg"
111+
width="16"
112+
height="16"
113+
viewBox="0 0 24 24"
114+
fill="none"
115+
stroke="currentColor"
116+
strokeWidth="2.5"
117+
strokeLinecap="round"
118+
strokeLinejoin="round"
119+
>
120+
<path d="M18 6 6 18" />
121+
<path d="m6 6 12 12" />
122+
</svg>
123+
</button>
124+
)}
125+
</div>
126+
)}
127+
128+
<div className={bodyClassName}>{children}</div>
129+
130+
{footer && <div className={footerClassName}>{footer}</div>}
131+
</div>
132+
</div>
133+
);
134+
135+
return createPortal(modalNode, portalContainer);
136+
}

src/components/common/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ export { default as Button } from './Button.astro';
66
export { default as Label } from './Label.astro';
77
export { default as FileUpload } from './FileUpload.astro';
88
export { default as Link } from './Link.astro';
9-
export { default as Modal } from './Modal.astro';
9+
export { default as ModalDialog } from './ModalDialog';

0 commit comments

Comments
 (0)