fix: page out of control

This commit is contained in:
wangcham
2025-10-22 13:37:53 +00:00
parent 345eccf04c
commit 6ba9b6973d
3 changed files with 183 additions and 11 deletions

View File

@@ -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<MCPComponentRef>(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<string, any>); // eslint-disable-line @typescript-eslint/no-explicit-any
}
@@ -277,7 +364,7 @@ export default function PluginConfigPage(
}
function handleFormSubmit(value: z.infer<typeof formSchema>) {
async function handleFormSubmit(value: z.infer<typeof formSchema>) {
const extraArgsObj: Record<string, string | number | boolean> = {};
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<string, string>,
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(
<Button
type="button"
variant="outline"
onClick={() => onFormCancel()}
onClick={() => {
setMcpSSEModalOpen(false);
form.reset();
setExtraArgs([]);
onFormCancel?.();
}}
>
{t('common.cancel')}
</Button>

View File

@@ -503,7 +503,7 @@ export class BackendClient extends BaseHttpClient {
public createMCPServer(
server: MCPServerConfig,
): Promise<AsyncTaskCreatedResp> {
return this.post('/api/v1/mcp/servers', server);
return this.post('/api/v1/mcp/servers', { source: server });
}
public updateMCPServer(

View File

@@ -7,9 +7,61 @@ import { XIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
function Dialog({
onOpenChange,
open,
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
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 <DialogPrimitive.Root data-slot="dialog" open={open} {...props} onOpenChange={handleOpenChange} />;
}
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}