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.
This commit is contained in:
Jason
2025-10-17 21:32:28 +08:00
parent bcaebc1bcb
commit 2d3d717826
6 changed files with 333 additions and 468 deletions

View File

@@ -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<EndpointSpeedTestProps> = ({
[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 (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
onMouseDown={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
{/* Backdrop */}
<div
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
isLinux() ? "" : " backdrop-blur-sm"
}`}
/>
{/* Modal */}
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-lg w-full max-w-2xl mx-4 max-h-[80vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
<h3 className="text-base font-medium text-gray-900 dark:text-gray-100">
{t("endpointTest.title")}
</h3>
<Button
type="button"
onClick={onClose}
variant="ghost"
size="icon"
aria-label={t("common.close")}
>
<X size={16} />
</Button>
</div>
<Dialog open={visible} onOpenChange={(open) => !open && onClose()}>
<DialogContent zIndex="nested" className="max-w-2xl max-h-[80vh] flex flex-col p-0">
<DialogHeader className="px-6 pt-6 pb-0">
<DialogTitle>{t("endpointTest.title")}</DialogTitle>
</DialogHeader>
{/* Content */}
<div className="flex-1 overflow-auto px-6 py-4 space-y-4">
@@ -635,15 +600,14 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
)}
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800">
<DialogFooter className="px-6 pb-6 pt-4 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800 m-0">
<Button type="button" onClick={onClose} className="gap-2">
<Save className="w-4 h-4" />
{t("common.save")}
</Button>
</div>
</div>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};