From 6ba9b6973dd3f5ee9e72e348af5fddf457194608 Mon Sep 17 00:00:00 2001 From: wangcham Date: Wed, 22 Oct 2025 13:37:53 +0000 Subject: [PATCH] fix: page out of control --- web/src/app/home/plugins/page.tsx | 137 ++++++++++++++++++++++-- web/src/app/infra/http/BackendClient.ts | 2 +- web/src/components/ui/dialog.tsx | 55 +++++++++- 3 files changed, 183 insertions(+), 11 deletions(-) diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index 4f071593..be4d6969 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -68,19 +68,19 @@ enum PluginInstallStatus { export default function PluginConfigPage( { - editMode, + editMode = false, initMCPId, onFormSubmit, onFormCancel, onMcpDeleted, }: { - editMode: boolean; + editMode?: boolean; initMCPId?: string; - onFormSubmit: () => void; - onFormCancel: () => void; - onMcpDeleted: () => void; - } + onFormSubmit?: () => void; + onFormCancel?: () => void; + onMcpDeleted?: () => void; + } = {} ) { const { t } = useTranslation(); @@ -227,6 +227,93 @@ export default function PluginConfigPage( const mcpComponentRef = useRef(null); const [mcpTesting, setMcpTesting] = useState(false); + // 强制清理 body 样式以修复 Dialog 关闭后点击失效的问题 + useEffect(() => { + console.log('[Dialog Debug] States:', { mcpSSEModalOpen, modalOpen, showDeleteConfirmModal }); + + if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) { + console.log('[Dialog Debug] All dialogs closed, cleaning up body styles...'); + console.log('[Dialog Debug] Before cleanup - body.style.pointerEvents:', document.body.style.pointerEvents); + console.log('[Dialog Debug] Before cleanup - body.style.overflow:', document.body.style.overflow); + + const cleanup = () => { + // 强制移除 body 上可能残留的样式 + document.body.style.removeProperty('pointer-events'); + document.body.style.removeProperty('overflow'); + + // 如果 removeProperty 不起作用,强制设置为空字符串 + if (document.body.style.pointerEvents === 'none') { + document.body.style.pointerEvents = ''; + } + if (document.body.style.overflow === 'hidden') { + document.body.style.overflow = ''; + } + + console.log('[Dialog Debug] After cleanup - body.style.pointerEvents:', document.body.style.pointerEvents); + console.log('[Dialog Debug] After cleanup - body.style.overflow:', document.body.style.overflow); + + // 检查计算后的样式 + const computedStyle = window.getComputedStyle(document.body); + console.log('[Dialog Debug] Computed pointerEvents:', computedStyle.pointerEvents); + }; + + // 多次清理以确保覆盖 Radix 的设置 + cleanup(); + const timer1 = setTimeout(cleanup, 0); + const timer2 = setTimeout(cleanup, 50); + const timer3 = setTimeout(cleanup, 100); + const timer4 = setTimeout(cleanup, 200); + const timer5 = setTimeout(cleanup, 300); + + return () => { + clearTimeout(timer1); + clearTimeout(timer2); + clearTimeout(timer3); + clearTimeout(timer4); + clearTimeout(timer5); + }; + } + }, [mcpSSEModalOpen, modalOpen, showDeleteConfirmModal]); + + // 额外的全局清理:定期检查并清理 + useEffect(() => { + const interval = setInterval(() => { + if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) { + if (document.body.style.pointerEvents === 'none') { + console.log('[Global Cleanup] Found stale pointer-events, cleaning...'); + document.body.style.removeProperty('pointer-events'); + document.body.style.pointerEvents = ''; + } + } + }, 500); + + return () => clearInterval(interval); + }, [mcpSSEModalOpen, modalOpen, showDeleteConfirmModal]); + + // MutationObserver:监视 body 的 style 变化 + useEffect(() => { + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'style') { + if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) { + if (document.body.style.pointerEvents === 'none') { + console.log('[MutationObserver] Detected pointer-events being set to none, reverting...'); + document.body.style.removeProperty('pointer-events'); + document.body.style.pointerEvents = ''; + } + } + } + }); + }); + + observer.observe(document.body, { + attributes: true, + attributeFilter: ['style'], + }); + + return () => observer.disconnect(); + }, [mcpSSEModalOpen, modalOpen, showDeleteConfirmModal]); + function handleModalConfirm() { installPlugin(installSource, installInfo as Record); // eslint-disable-line @typescript-eslint/no-explicit-any } @@ -277,7 +364,7 @@ export default function PluginConfigPage( } - function handleFormSubmit(value: z.infer) { + async function handleFormSubmit(value: z.infer) { const extraArgsObj: Record = {}; value.extra_args?.forEach( (arg: { key: string; type: string; value: string }) => { @@ -290,6 +377,35 @@ export default function PluginConfigPage( } }, ); + + try { + // 构造符合 MCPServerConfig 类型的数据 + const serverConfig = { + name: value.name, + mode: 'sse' as const, + enable: true, + url: value.url, + headers: extraArgsObj as Record, + timeout: value.timeout, + }; + + await httpClient.createMCPServer(serverConfig); + + toast.success(t('mcp.createSuccess')); + + // 只有在异步操作成功后才关闭对话框 + setMcpSSEModalOpen(false); + + // 重置表单 + form.reset(); + setExtraArgs([]); + + // 调用回调通知父组件刷新 + onFormSubmit?.(); + } catch (error) { + console.error('Failed to create MCP server:', error); + toast.error(t('mcp.createFailed')); + } } function testMcp() { @@ -872,7 +988,12 @@ export default function PluginConfigPage( diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index 152a5082..8c533ab2 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -503,7 +503,7 @@ export class BackendClient extends BaseHttpClient { public createMCPServer( server: MCPServerConfig, ): Promise { - return this.post('/api/v1/mcp/servers', server); + return this.post('/api/v1/mcp/servers', { source: server }); } public updateMCPServer( diff --git a/web/src/components/ui/dialog.tsx b/web/src/components/ui/dialog.tsx index 7caae6a2..91823fd2 100644 --- a/web/src/components/ui/dialog.tsx +++ b/web/src/components/ui/dialog.tsx @@ -7,9 +7,61 @@ import { XIcon } from 'lucide-react'; import { cn } from '@/lib/utils'; function Dialog({ + onOpenChange, + open, ...props }: React.ComponentProps) { - return ; + const handleOpenChange = React.useCallback((isOpen: boolean) => { + onOpenChange?.(isOpen); + + // 当对话框关闭时,确保清理 body 样式 + if (!isOpen) { + // 立即清理 + document.body.style.removeProperty('pointer-events'); + document.body.style.removeProperty('overflow'); + + // 延迟再次清理,确保覆盖 Radix 的设置 + setTimeout(() => { + document.body.style.removeProperty('pointer-events'); + document.body.style.removeProperty('overflow'); + }, 0); + + setTimeout(() => { + document.body.style.removeProperty('pointer-events'); + document.body.style.removeProperty('overflow'); + }, 50); + + setTimeout(() => { + document.body.style.removeProperty('pointer-events'); + document.body.style.removeProperty('overflow'); + }, 150); + } + }, [onOpenChange]); + + // 使用 effect 监控 open 状态变化 + React.useEffect(() => { + if (open === false) { + const cleanup = () => { + document.body.style.removeProperty('pointer-events'); + document.body.style.removeProperty('overflow'); + }; + + cleanup(); + const timer1 = setTimeout(cleanup, 0); + const timer2 = setTimeout(cleanup, 50); + const timer3 = setTimeout(cleanup, 150); + const timer4 = setTimeout(cleanup, 300); + + return () => { + clearTimeout(timer1); + clearTimeout(timer2); + clearTimeout(timer3); + clearTimeout(timer4); + }; + } + }, [open]); + + return ; } function DialogTrigger({ @@ -60,7 +112,6 @@ function DialogContent({ 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg', className, )} - onInteractOutside={() => {}} {...props} > {children}