From 2d3d717826bcc7ed009c00b1ee51b2ed3b40285f Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 17 Oct 2025 21:32:28 +0800 Subject: [PATCH] refactor: unify modal overlay system with shadcn/ui Dialog Fix inconsistent modal overlays by migrating all custom implementations to the unified shadcn/ui Dialog component with proper z-index layering. Changes: - Update Dialog component to support three z-index levels: - base (z-40): First-level dialogs - nested (z-50): Nested dialogs - alert (z-[60]): Alert/confirmation dialogs (using Tailwind arbitrary value) - Refactor all custom modal implementations to use Dialog: - EndpointSpeedTest: API endpoint speed testing panel - ClaudeConfigEditor: Claude common config editor - CodexQuickWizardModal: Codex quick setup wizard - CodexCommonConfigModal: Codex common config editor - SettingsDialog: Restart confirmation prompt - Remove custom backdrop implementations and manual z-index - Leverage Radix UI Portal for automatic DOM order management - Ensure consistent overlay behavior and keyboard interactions This eliminates the "background residue" issue where overlays from different layers would conflict, providing a unified and professional user experience across all modal interactions. --- .../providers/forms/ClaudeConfigEditor.tsx | 122 ++---- .../forms/CodexCommonConfigModal.tsx | 92 ++--- .../providers/forms/CodexQuickWizardModal.tsx | 390 ++++++++---------- .../providers/forms/EndpointSpeedTest.tsx | 68 +-- src/components/settings/SettingsDialog.tsx | 45 +- src/components/ui/dialog.tsx | 84 ++-- 6 files changed, 333 insertions(+), 468 deletions(-) diff --git a/src/components/providers/forms/ClaudeConfigEditor.tsx b/src/components/providers/forms/ClaudeConfigEditor.tsx index a340f90..2e5e43c 100644 --- a/src/components/providers/forms/ClaudeConfigEditor.tsx +++ b/src/components/providers/forms/ClaudeConfigEditor.tsx @@ -1,8 +1,15 @@ import React, { useEffect, useState } from "react"; import JsonEditor from "@/components/JsonEditor"; -import { X, Save } from "lucide-react"; -import { isLinux } from "@/lib/platform"; +import { Save } from "lucide-react"; import { useTranslation } from "react-i18next"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; interface ClaudeConfigEditorProps { value: string; @@ -60,20 +67,6 @@ const ClaudeConfigEditor: React.FC = ({ } }, [commonConfigError, isCommonConfigModalOpen]); - // 支持按下 ESC 关闭弹窗 - useEffect(() => { - if (!isCommonConfigModalOpen) return; - - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - e.preventDefault(); - closeModal(); - } - }; - window.addEventListener("keydown", onKeyDown); - return () => window.removeEventListener("keydown", onKeyDown); - }, [isCommonConfigModalOpen]); - const closeModal = () => { setIsCommonConfigModalOpen(false); }; @@ -128,76 +121,41 @@ const ClaudeConfigEditor: React.FC = ({

{t("claudeConfig.fullSettingsHint")}

- {isCommonConfigModalOpen && ( -
{ - if (e.target === e.currentTarget) closeModal(); - }} - > - {/* Backdrop - 统一背景样式 */} -
- {/* Modal - 统一窗口样式 */} -
- {/* Header - 统一标题栏样式 */} -
-

- {t("claudeConfig.editCommonConfigTitle")} -

- -
+ !open && closeModal()}> + + + {t("claudeConfig.editCommonConfigTitle")} + - {/* Content - 统一内容区域样式 */} -
-

- {t("claudeConfig.commonConfigHint")} +

+

+ {t("claudeConfig.commonConfigHint")} +

+ + {commonConfigError && ( +

+ {commonConfigError}

- - {commonConfigError && ( -

- {commonConfigError} -

- )} -
- - {/* Footer - 统一底部按钮样式 */} -
- - -
+ )}
-
- )} + + + + + + +
); }; diff --git a/src/components/providers/forms/CodexCommonConfigModal.tsx b/src/components/providers/forms/CodexCommonConfigModal.tsx index c836972..53f97db 100644 --- a/src/components/providers/forms/CodexCommonConfigModal.tsx +++ b/src/components/providers/forms/CodexCommonConfigModal.tsx @@ -1,7 +1,14 @@ -import React, { useEffect } from "react"; -import { X, Save } from "lucide-react"; +import React from "react"; +import { Save } from "lucide-react"; import { useTranslation } from "react-i18next"; -import { isLinux } from "@/lib/platform"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; interface CodexCommonConfigModalProps { isOpen: boolean; @@ -24,56 +31,14 @@ export const CodexCommonConfigModal: React.FC = ({ }) => { const { t } = useTranslation(); - // Support ESC key to close modal - useEffect(() => { - if (!isOpen) return; - - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - e.preventDefault(); - onClose(); - } - }; - - window.addEventListener("keydown", onKeyDown); - return () => window.removeEventListener("keydown", onKeyDown); - }, [isOpen, onClose]); - - if (!isOpen) return null; - return ( -
{ - if (e.target === e.currentTarget) onClose(); - }} - > - {/* Backdrop */} -
+ !open && onClose()}> + + + {t("codexConfig.editCommonConfigTitle")} + - {/* Modal */} -
- {/* Header */} -
-

- {t("codexConfig.editCommonConfigTitle")} -

- -
- - {/* Content */} -
+

{t("codexConfig.commonConfigHint")}

@@ -102,25 +67,16 @@ export const CodexCommonConfigModal: React.FC = ({ )}
- {/* Footer */} -
- - + -
-
-
+ + +
+
); }; diff --git a/src/components/providers/forms/CodexQuickWizardModal.tsx b/src/components/providers/forms/CodexQuickWizardModal.tsx index 785f081..119b809 100644 --- a/src/components/providers/forms/CodexQuickWizardModal.tsx +++ b/src/components/providers/forms/CodexQuickWizardModal.tsx @@ -1,11 +1,19 @@ import React, { useState, useRef } from "react"; -import { X, Save } from "lucide-react"; +import { Save } from "lucide-react"; import { useTranslation } from "react-i18next"; -import { isLinux } from "@/lib/platform"; import { generateThirdPartyAuth, generateThirdPartyConfig, } from "@/config/codexProviderPresets"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; interface CodexQuickWizardModalProps { isOpen: boolean; @@ -101,225 +109,187 @@ export const CodexQuickWizardModal: React.FC = ({ } }; - if (!isOpen) return null; - return ( -
{ - if (e.target === e.currentTarget) { - handleClose(); - } - }} - > -
+ !open && handleClose()}> + + + {t("codexConfig.quickWizard")} + -
-
- {/* Header */} -
-

- {t("codexConfig.quickWizard")} -

- +
+
+

+ {t("codexConfig.wizardHint")} +

- {/* Content */} -
-
-

- {t("codexConfig.wizardHint")} +

+ {/* API Key */} +
+ + setTemplateApiKey(e.target.value)} + onKeyDown={handleInputKeyDown} + pattern=".*\S.*" + title={t("common.enterValidValue")} + placeholder={t("codexConfig.apiKeyPlaceholder")} + required + className="font-mono" + /> +
+ + {/* Display Name */} +
+ + setTemplateDisplayName(e.target.value)} + onKeyDown={handleInputKeyDown} + placeholder={t("codexConfig.supplierNamePlaceholder")} + required + pattern=".*\S.*" + title={t("common.enterValidValue")} + /> +

+ {t("codexConfig.supplierNameHint")}

-
- {/* API Key */} -
- - setTemplateApiKey(e.target.value)} - onKeyDown={handleInputKeyDown} - pattern=".*\S.*" - title={t("common.enterValidValue")} - placeholder={t("codexConfig.apiKeyPlaceholder")} - required - className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" - /> -
- - {/* Display Name */} -
- - setTemplateDisplayName(e.target.value)} - onKeyDown={handleInputKeyDown} - placeholder={t("codexConfig.supplierNamePlaceholder")} - required - pattern=".*\S.*" - title={t("common.enterValidValue")} - className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" - /> -

- {t("codexConfig.supplierNameHint")} -

-
- - {/* Provider Name */} -
- - setTemplateProviderName(e.target.value)} - onKeyDown={handleInputKeyDown} - placeholder={t("codexConfig.supplierCodePlaceholder")} - className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" - /> -

- {t("codexConfig.supplierCodeHint")} -

-
- - {/* Base URL */} -
- - setTemplateBaseUrl(e.target.value)} - onKeyDown={handleInputKeyDown} - placeholder={t("codexConfig.apiUrlPlaceholder")} - required - className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" - /> -
- - {/* Website URL */} -
- - setTemplateWebsiteUrl(e.target.value)} - onKeyDown={handleInputKeyDown} - placeholder={t("codexConfig.websitePlaceholder")} - className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" - /> -

- {t("codexConfig.websiteHint")} -

-
- - {/* Model Name */} -
- - setTemplateModelName(e.target.value)} - onKeyDown={handleInputKeyDown} - pattern=".*\S.*" - title={t("common.enterValidValue")} - placeholder={t("codexConfig.modelNamePlaceholder")} - required - className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" - /> -
+ {/* Provider Name */} +
+ + setTemplateProviderName(e.target.value)} + onKeyDown={handleInputKeyDown} + placeholder={t("codexConfig.supplierCodePlaceholder")} + /> +

+ {t("codexConfig.supplierCodeHint")} +

- {/* Preview */} - {(templateApiKey || templateProviderName || templateBaseUrl) && ( -
-

- {t("codexConfig.configPreview")} -

-
-
- -
-                      {JSON.stringify(
-                        generateThirdPartyAuth(templateApiKey),
-                        null,
-                        2,
-                      )}
-                    
-
-
- -
-                      {templateProviderName && templateBaseUrl
-                        ? generateThirdPartyConfig(
-                            templateProviderName,
-                            templateBaseUrl,
-                            templateModelName,
-                          )
-                        : ""}
-                    
-
-
-
- )} + {/* Base URL */} +
+ + setTemplateBaseUrl(e.target.value)} + onKeyDown={handleInputKeyDown} + placeholder={t("codexConfig.apiUrlPlaceholder")} + required + className="font-mono" + /> +
+ + {/* Website URL */} +
+ + setTemplateWebsiteUrl(e.target.value)} + onKeyDown={handleInputKeyDown} + placeholder={t("codexConfig.websitePlaceholder")} + /> +

+ {t("codexConfig.websiteHint")} +

+
+ + {/* Model Name */} +
+ + setTemplateModelName(e.target.value)} + onKeyDown={handleInputKeyDown} + pattern=".*\S.*" + title={t("common.enterValidValue")} + placeholder={t("codexConfig.modelNamePlaceholder")} + required + /> +
- {/* Footer */} -
- - -
+ {/* Preview */} + {(templateApiKey || templateProviderName || templateBaseUrl) && ( +
+

+ {t("codexConfig.configPreview")} +

+
+
+ +
+                    {JSON.stringify(
+                      generateThirdPartyAuth(templateApiKey),
+                      null,
+                      2,
+                    )}
+                  
+
+
+ +
+                    {templateProviderName && templateBaseUrl
+                      ? generateThirdPartyConfig(
+                          templateProviderName,
+                          templateBaseUrl,
+                          templateModelName,
+                        )
+                      : ""}
+                  
+
+
+
+ )}
-
-
+ + + + + + +
); }; diff --git a/src/components/providers/forms/EndpointSpeedTest.tsx b/src/components/providers/forms/EndpointSpeedTest.tsx index fd74967..7704b27 100644 --- a/src/components/providers/forms/EndpointSpeedTest.tsx +++ b/src/components/providers/forms/EndpointSpeedTest.tsx @@ -1,11 +1,17 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Zap, Loader2, Plus, X, AlertCircle, Save } from "lucide-react"; -import { isLinux } from "@/lib/platform"; import type { AppType } from "@/lib/api"; import { vscodeApi } from "@/lib/api/vscode"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; // 临时类型定义,待后端 API 实现后替换 @@ -422,53 +428,12 @@ const EndpointSpeedTest: React.FC = ({ [normalizedSelected, onChange, appType, entries, providerId], ); - // 支持按下 ESC 关闭弹窗 - useEffect(() => { - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - e.preventDefault(); - onClose(); - } - }; - window.addEventListener("keydown", onKeyDown); - return () => window.removeEventListener("keydown", onKeyDown); - }, [onClose]); - - if (!visible) { - return null; - } - return ( -
{ - if (e.target === e.currentTarget) onClose(); - }} - > - {/* Backdrop */} -
- - {/* Modal */} -
- {/* Header */} -
-

- {t("endpointTest.title")} -

- -
+ !open && onClose()}> + + + {t("endpointTest.title")} + {/* Content */}
@@ -635,15 +600,14 @@ const EndpointSpeedTest: React.FC = ({ )}
- {/* Footer */} -
+ -
-
-
+ + + ); }; diff --git a/src/components/settings/SettingsDialog.tsx b/src/components/settings/SettingsDialog.tsx index 95fafd7..540e34e 100644 --- a/src/components/settings/SettingsDialog.tsx +++ b/src/components/settings/SettingsDialog.tsx @@ -275,31 +275,26 @@ export function SettingsDialog({ - {showRestartPrompt ? ( -
-
-
-
-

- {t("settings.restartRequired")} -

-

- {t("settings.restartRequiredMessage", { - defaultValue: "配置目录已变更,需要重启应用生效。", - })} -

-
-
- - -
-
-
- ) : null} + !open && handleRestartLater()}> + + + {t("settings.restartRequired")} + +

+ {t("settings.restartRequiredMessage", { + defaultValue: "配置目录已变更,需要重启应用生效。", + })} +

+ + + + +
+
); } diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 14effa3..8c90de3 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -13,41 +13,63 @@ const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); + React.ComponentPropsWithoutRef & { + zIndex?: "base" | "nested" | "alert"; + } +>(({ className, zIndex = "base", ...props }, ref) => { + const zIndexMap = { + base: "z-40", + nested: "z-50", + alert: "z-[60]", + }; + + return ( + + ); +}); DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - {children} - - - 关闭 - - - -)); + React.ComponentPropsWithoutRef & { + zIndex?: "base" | "nested" | "alert"; + } +>(({ className, children, zIndex = "base", ...props }, ref) => { + const zIndexMap = { + base: "z-40", + nested: "z-50", + alert: "z-[60]", + }; + + return ( + + + + {children} + + + 关闭 + + + + ); +}); DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({