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:
@@ -1,8 +1,15 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import JsonEditor from "@/components/JsonEditor";
|
import JsonEditor from "@/components/JsonEditor";
|
||||||
import { X, Save } from "lucide-react";
|
import { Save } from "lucide-react";
|
||||||
import { isLinux } from "@/lib/platform";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
interface ClaudeConfigEditorProps {
|
interface ClaudeConfigEditorProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -60,20 +67,6 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
|
|||||||
}
|
}
|
||||||
}, [commonConfigError, isCommonConfigModalOpen]);
|
}, [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 = () => {
|
const closeModal = () => {
|
||||||
setIsCommonConfigModalOpen(false);
|
setIsCommonConfigModalOpen(false);
|
||||||
};
|
};
|
||||||
@@ -128,39 +121,14 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
|
|||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{t("claudeConfig.fullSettingsHint")}
|
{t("claudeConfig.fullSettingsHint")}
|
||||||
</p>
|
</p>
|
||||||
{isCommonConfigModalOpen && (
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
if (e.target === e.currentTarget) closeModal();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Backdrop - 统一背景样式 */}
|
|
||||||
<div
|
|
||||||
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
|
||||||
isLinux() ? "" : " backdrop-blur-sm"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Modal - 统一窗口样式 */}
|
<Dialog open={isCommonConfigModalOpen} onOpenChange={(open) => !open && closeModal()}>
|
||||||
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-lg max-w-2xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
<DialogContent zIndex="nested" className="max-w-2xl max-h-[90vh] flex flex-col p-0">
|
||||||
{/* Header - 统一标题栏样式 */}
|
<DialogHeader className="px-6 pt-6 pb-0">
|
||||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
<DialogTitle>{t("claudeConfig.editCommonConfigTitle")}</DialogTitle>
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
</DialogHeader>
|
||||||
{t("claudeConfig.editCommonConfigTitle")}
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={closeModal}
|
|
||||||
className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
|
||||||
aria-label={t("common.close")}
|
|
||||||
>
|
|
||||||
<X size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content - 统一内容区域样式 */}
|
<div className="flex-1 overflow-auto px-6 py-4 space-y-4">
|
||||||
<div className="flex-1 overflow-auto p-6 space-y-4">
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{t("claudeConfig.commonConfigHint")}
|
{t("claudeConfig.commonConfigHint")}
|
||||||
</p>
|
</p>
|
||||||
@@ -177,27 +145,17 @@ const ClaudeConfigEditor: React.FC<ClaudeConfigEditorProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer - 统一底部按钮样式 */}
|
<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">
|
||||||
<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">
|
<Button type="button" variant="outline" onClick={closeModal}>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={closeModal}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-white dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
{t("common.cancel")}
|
{t("common.cancel")}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button type="button" onClick={closeModal} className="gap-2">
|
||||||
type="button"
|
|
||||||
onClick={closeModal}
|
|
||||||
className="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Save className="w-4 h-4" />
|
<Save className="w-4 h-4" />
|
||||||
{t("common.save")}
|
{t("common.save")}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</div>
|
</DialogContent>
|
||||||
</div>
|
</Dialog>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import React, { useEffect } from "react";
|
import React from "react";
|
||||||
import { X, Save } from "lucide-react";
|
import { Save } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 {
|
interface CodexCommonConfigModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -24,56 +31,14 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<div
|
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
<DialogContent zIndex="nested" className="max-w-2xl max-h-[90vh] flex flex-col p-0">
|
||||||
onMouseDown={(e) => {
|
<DialogHeader className="px-6 pt-6 pb-0">
|
||||||
if (e.target === e.currentTarget) onClose();
|
<DialogTitle>{t("codexConfig.editCommonConfigTitle")}</DialogTitle>
|
||||||
}}
|
</DialogHeader>
|
||||||
>
|
|
||||||
{/* Backdrop */}
|
|
||||||
<div
|
|
||||||
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
|
||||||
isLinux() ? "" : " backdrop-blur-sm"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Modal */}
|
<div className="flex-1 overflow-auto px-6 py-4 space-y-4">
|
||||||
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-lg max-w-2xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{t("codexConfig.editCommonConfigTitle")}
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
|
||||||
aria-label={t("common.close")}
|
|
||||||
>
|
|
||||||
<X size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1 overflow-auto p-6 space-y-4">
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{t("codexConfig.commonConfigHint")}
|
{t("codexConfig.commonConfigHint")}
|
||||||
</p>
|
</p>
|
||||||
@@ -102,25 +67,16 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
<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">
|
||||||
<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">
|
<Button type="button" variant="outline" onClick={onClose}>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-white dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
{t("common.cancel")}
|
{t("common.cancel")}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button type="button" onClick={onClose} className="gap-2">
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Save className="w-4 h-4" />
|
<Save className="w-4 h-4" />
|
||||||
{t("common.save")}
|
{t("common.save")}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</div>
|
</DialogContent>
|
||||||
</div>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import React, { useState, useRef } from "react";
|
import React, { useState, useRef } from "react";
|
||||||
import { X, Save } from "lucide-react";
|
import { Save } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { isLinux } from "@/lib/platform";
|
|
||||||
import {
|
import {
|
||||||
generateThirdPartyAuth,
|
generateThirdPartyAuth,
|
||||||
generateThirdPartyConfig,
|
generateThirdPartyConfig,
|
||||||
} from "@/config/codexProviderPresets";
|
} 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 {
|
interface CodexQuickWizardModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -101,42 +109,14 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
<DialogContent zIndex="nested" className="max-w-2xl max-h-[90vh] flex flex-col p-0">
|
||||||
onMouseDown={(e) => {
|
<DialogHeader className="px-6 pt-6 pb-0">
|
||||||
if (e.target === e.currentTarget) {
|
<DialogTitle>{t("codexConfig.quickWizard")}</DialogTitle>
|
||||||
handleClose();
|
</DialogHeader>
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
|
||||||
isLinux() ? "" : " backdrop-blur-sm"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative mx-4 flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl bg-white shadow-lg dark:bg-gray-900">
|
<div className="flex-1 min-h-0 space-y-4 overflow-auto px-6 py-4">
|
||||||
<div className="flex h-full min-h-0 flex-col" role="form">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between border-b border-gray-200 p-6 dark:border-gray-800">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{t("codexConfig.quickWizard")}
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleClose}
|
|
||||||
className="rounded-md p-1 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100"
|
|
||||||
aria-label={t("common.close")}
|
|
||||||
>
|
|
||||||
<X size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1 min-h-0 space-y-4 overflow-auto p-6">
|
|
||||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
|
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
|
||||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
{t("codexConfig.wizardHint")}
|
{t("codexConfig.wizardHint")}
|
||||||
@@ -149,7 +129,7 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
|
|||||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
{t("codexConfig.apiKeyLabel")}
|
{t("codexConfig.apiKeyLabel")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={templateApiKey}
|
value={templateApiKey}
|
||||||
ref={apiKeyInputRef}
|
ref={apiKeyInputRef}
|
||||||
@@ -159,7 +139,7 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
|
|||||||
title={t("common.enterValidValue")}
|
title={t("common.enterValidValue")}
|
||||||
placeholder={t("codexConfig.apiKeyPlaceholder")}
|
placeholder={t("codexConfig.apiKeyPlaceholder")}
|
||||||
required
|
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"
|
className="font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -168,7 +148,7 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
|
|||||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
{t("codexConfig.supplierNameLabel")}
|
{t("codexConfig.supplierNameLabel")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={templateDisplayName}
|
value={templateDisplayName}
|
||||||
ref={displayNameInputRef}
|
ref={displayNameInputRef}
|
||||||
@@ -178,7 +158,6 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
|
|||||||
required
|
required
|
||||||
pattern=".*\S.*"
|
pattern=".*\S.*"
|
||||||
title={t("common.enterValidValue")}
|
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"
|
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{t("codexConfig.supplierNameHint")}
|
{t("codexConfig.supplierNameHint")}
|
||||||
@@ -190,13 +169,12 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
|
|||||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
{t("codexConfig.supplierCodeLabel")}
|
{t("codexConfig.supplierCodeLabel")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={templateProviderName}
|
value={templateProviderName}
|
||||||
onChange={(e) => setTemplateProviderName(e.target.value)}
|
onChange={(e) => setTemplateProviderName(e.target.value)}
|
||||||
onKeyDown={handleInputKeyDown}
|
onKeyDown={handleInputKeyDown}
|
||||||
placeholder={t("codexConfig.supplierCodePlaceholder")}
|
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"
|
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{t("codexConfig.supplierCodeHint")}
|
{t("codexConfig.supplierCodeHint")}
|
||||||
@@ -208,7 +186,7 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
|
|||||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
{t("codexConfig.apiUrlLabel")}
|
{t("codexConfig.apiUrlLabel")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Input
|
||||||
type="url"
|
type="url"
|
||||||
value={templateBaseUrl}
|
value={templateBaseUrl}
|
||||||
ref={baseUrlInputRef}
|
ref={baseUrlInputRef}
|
||||||
@@ -216,7 +194,7 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
|
|||||||
onKeyDown={handleInputKeyDown}
|
onKeyDown={handleInputKeyDown}
|
||||||
placeholder={t("codexConfig.apiUrlPlaceholder")}
|
placeholder={t("codexConfig.apiUrlPlaceholder")}
|
||||||
required
|
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"
|
className="font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -225,13 +203,12 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
|
|||||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
{t("codexConfig.websiteLabel")}
|
{t("codexConfig.websiteLabel")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Input
|
||||||
type="url"
|
type="url"
|
||||||
value={templateWebsiteUrl}
|
value={templateWebsiteUrl}
|
||||||
onChange={(e) => setTemplateWebsiteUrl(e.target.value)}
|
onChange={(e) => setTemplateWebsiteUrl(e.target.value)}
|
||||||
onKeyDown={handleInputKeyDown}
|
onKeyDown={handleInputKeyDown}
|
||||||
placeholder={t("codexConfig.websitePlaceholder")}
|
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"
|
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
{t("codexConfig.websiteHint")}
|
{t("codexConfig.websiteHint")}
|
||||||
@@ -243,7 +220,7 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
|
|||||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
{t("codexConfig.modelNameLabel")}
|
{t("codexConfig.modelNameLabel")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={templateModelName}
|
value={templateModelName}
|
||||||
ref={modelNameInputRef}
|
ref={modelNameInputRef}
|
||||||
@@ -253,7 +230,6 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
|
|||||||
title={t("common.enterValidValue")}
|
title={t("common.enterValidValue")}
|
||||||
placeholder={t("codexConfig.modelNamePlaceholder")}
|
placeholder={t("codexConfig.modelNamePlaceholder")}
|
||||||
required
|
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"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -296,30 +272,24 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
<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">
|
||||||
<div className="flex items-center justify-end gap-3 border-t border-gray-200 bg-gray-100 p-6 dark:border-gray-800 dark:bg-gray-800">
|
<Button type="button" variant="outline" onClick={handleClose}>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleClose}
|
|
||||||
className="rounded-lg px-4 py-2 text-sm font-medium text-gray-500 transition-colors hover:bg-white hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-100"
|
|
||||||
>
|
|
||||||
{t("common.cancel")}
|
{t("common.cancel")}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
applyTemplate();
|
applyTemplate();
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-2 rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
<Save className="h-4 w-4" />
|
<Save className="h-4 w-4" />
|
||||||
{t("codexConfig.applyConfig")}
|
{t("codexConfig.applyConfig")}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</div>
|
</DialogContent>
|
||||||
</div>
|
</Dialog>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Zap, Loader2, Plus, X, AlertCircle, Save } from "lucide-react";
|
import { Zap, Loader2, Plus, X, AlertCircle, Save } from "lucide-react";
|
||||||
import { isLinux } from "@/lib/platform";
|
|
||||||
import type { AppType } from "@/lib/api";
|
import type { AppType } from "@/lib/api";
|
||||||
import { vscodeApi } from "@/lib/api/vscode";
|
import { vscodeApi } from "@/lib/api/vscode";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
|
||||||
// 临时类型定义,待后端 API 实现后替换
|
// 临时类型定义,待后端 API 实现后替换
|
||||||
@@ -422,53 +428,12 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
[normalizedSelected, onChange, appType, entries, providerId],
|
[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 (
|
return (
|
||||||
<div
|
<Dialog open={visible} onOpenChange={(open) => !open && onClose()}>
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
<DialogContent zIndex="nested" className="max-w-2xl max-h-[80vh] flex flex-col p-0">
|
||||||
onMouseDown={(e) => {
|
<DialogHeader className="px-6 pt-6 pb-0">
|
||||||
if (e.target === e.currentTarget) onClose();
|
<DialogTitle>{t("endpointTest.title")}</DialogTitle>
|
||||||
}}
|
</DialogHeader>
|
||||||
>
|
|
||||||
{/* 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>
|
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-auto px-6 py-4 space-y-4">
|
<div className="flex-1 overflow-auto px-6 py-4 space-y-4">
|
||||||
@@ -635,15 +600,14 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
<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">
|
||||||
<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">
|
|
||||||
<Button type="button" onClick={onClose} className="gap-2">
|
<Button type="button" onClick={onClose} className="gap-2">
|
||||||
<Save className="w-4 h-4" />
|
<Save className="w-4 h-4" />
|
||||||
{t("common.save")}
|
{t("common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</div>
|
</DialogContent>
|
||||||
</div>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -275,31 +275,26 @@ export function SettingsDialog({
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
{showRestartPrompt ? (
|
<Dialog open={showRestartPrompt} onOpenChange={(open) => !open && handleRestartLater()}>
|
||||||
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
<DialogContent zIndex="alert" className="max-w-md">
|
||||||
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm" />
|
<DialogHeader>
|
||||||
<div className="relative z-10 w-full max-w-md space-y-4 rounded-lg border border-border bg-background p-6 shadow-xl">
|
<DialogTitle>{t("settings.restartRequired")}</DialogTitle>
|
||||||
<div className="space-y-1">
|
</DialogHeader>
|
||||||
<h2 className="text-lg font-semibold">
|
|
||||||
{t("settings.restartRequired")}
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t("settings.restartRequiredMessage", {
|
{t("settings.restartRequiredMessage", {
|
||||||
defaultValue: "配置目录已变更,需要重启应用生效。",
|
defaultValue: "配置目录已变更,需要重启应用生效。",
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<DialogFooter className="gap-2">
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button variant="outline" onClick={handleRestartLater}>
|
<Button variant="outline" onClick={handleRestartLater}>
|
||||||
{t("settings.restartLater")}
|
{t("settings.restartLater")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleRestartNow}>
|
<Button onClick={handleRestartNow}>
|
||||||
{t("settings.restartNow")}
|
{t("settings.restartNow")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</div>
|
</DialogContent>
|
||||||
</div>
|
</Dialog>
|
||||||
) : null}
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,29 +13,50 @@ const DialogClose = DialogPrimitive.Close;
|
|||||||
|
|
||||||
const DialogOverlay = React.forwardRef<
|
const DialogOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> & {
|
||||||
>(({ className, ...props }, ref) => (
|
zIndex?: "base" | "nested" | "alert";
|
||||||
|
}
|
||||||
|
>(({ className, zIndex = "base", ...props }, ref) => {
|
||||||
|
const zIndexMap = {
|
||||||
|
base: "z-40",
|
||||||
|
nested: "z-50",
|
||||||
|
alert: "z-[60]",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
"fixed inset-0 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
zIndexMap[zIndex],
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
);
|
||||||
|
});
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||||
>(({ className, children, ...props }, ref) => (
|
zIndex?: "base" | "nested" | "alert";
|
||||||
|
}
|
||||||
|
>(({ className, children, zIndex = "base", ...props }, ref) => {
|
||||||
|
const zIndexMap = {
|
||||||
|
base: "z-40",
|
||||||
|
nested: "z-50",
|
||||||
|
alert: "z-[60]",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay />
|
<DialogOverlay zIndex={zIndex} />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-white dark:bg-gray-900 p-6 shadow-lg duration-200 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 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
"fixed left-1/2 top-1/2 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-white dark:bg-gray-900 p-6 shadow-lg duration-200 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 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
zIndexMap[zIndex],
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -47,7 +68,8 @@ const DialogContent = React.forwardRef<
|
|||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
));
|
);
|
||||||
|
});
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DialogHeader = ({
|
const DialogHeader = ({
|
||||||
|
|||||||
Reference in New Issue
Block a user