mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
feat(ui): implement global ConfirmationModal to replace native window.confirm
This commit is contained in:
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
import { HashRouter, Route, Routes } from 'react-router-dom';
|
import { HashRouter, Route, Routes } from 'react-router-dom';
|
||||||
import { LoginPage } from '@/pages/LoginPage';
|
import { LoginPage } from '@/pages/LoginPage';
|
||||||
import { NotificationContainer } from '@/components/common/NotificationContainer';
|
import { NotificationContainer } from '@/components/common/NotificationContainer';
|
||||||
|
import { ConfirmationModal } from '@/components/common/ConfirmationModal';
|
||||||
import { SplashScreen } from '@/components/common/SplashScreen';
|
import { SplashScreen } from '@/components/common/SplashScreen';
|
||||||
import { MainLayout } from '@/components/layout/MainLayout';
|
import { MainLayout } from '@/components/layout/MainLayout';
|
||||||
import { ProtectedRoute } from '@/router/ProtectedRoute';
|
import { ProtectedRoute } from '@/router/ProtectedRoute';
|
||||||
@@ -61,6 +62,7 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<NotificationContainer />
|
<NotificationContainer />
|
||||||
|
<ConfirmationModal />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
58
src/components/common/ConfirmationModal.tsx
Normal file
58
src/components/common/ConfirmationModal.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Modal } from '@/components/ui/Modal';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { useNotificationStore } from '@/stores';
|
||||||
|
|
||||||
|
export function ConfirmationModal() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const confirmation = useNotificationStore((state) => state.confirmation);
|
||||||
|
const hideConfirmation = useNotificationStore((state) => state.hideConfirmation);
|
||||||
|
const setConfirmationLoading = useNotificationStore((state) => state.setConfirmationLoading);
|
||||||
|
|
||||||
|
const { isOpen, isLoading, options } = confirmation;
|
||||||
|
|
||||||
|
if (!isOpen || !options) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, message, onConfirm, onCancel, confirmText, cancelText, variant = 'primary' } = options;
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
try {
|
||||||
|
setConfirmationLoading(true);
|
||||||
|
await onConfirm();
|
||||||
|
hideConfirmation();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Confirmation action failed:', error);
|
||||||
|
// Optional: show error notification here if needed,
|
||||||
|
// but usually the calling component handles specific errors.
|
||||||
|
} finally {
|
||||||
|
setConfirmationLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
hideConfirmation();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={isOpen} onClose={handleCancel} title={title}>
|
||||||
|
<p style={{ margin: '1rem 0' }}>{message}</p>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '2rem' }}>
|
||||||
|
<Button variant="ghost" onClick={handleCancel} disabled={isLoading}>
|
||||||
|
{cancelText || t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={variant}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
{confirmText || t('common.confirm')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import styles from './ApiKeysPage.module.scss';
|
|||||||
|
|
||||||
export function ApiKeysPage() {
|
export function ApiKeysPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { showNotification } = useNotificationStore();
|
const { showNotification, showConfirmation } = useNotificationStore();
|
||||||
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
||||||
|
|
||||||
const config = useConfigStore((state) => state.config);
|
const config = useConfigStore((state) => state.config);
|
||||||
@@ -29,7 +29,6 @@ export function ApiKeysPage() {
|
|||||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [deletingIndex, setDeletingIndex] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const disableControls = useMemo(() => connectionStatus !== 'connected', [connectionStatus]);
|
const disableControls = useMemo(() => connectionStatus !== 'connected', [connectionStatus]);
|
||||||
|
|
||||||
@@ -115,21 +114,24 @@ export function ApiKeysPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (index: number) => {
|
const handleDelete = (index: number) => {
|
||||||
if (!window.confirm(t('api_keys.delete_confirm'))) return;
|
showConfirmation({
|
||||||
setDeletingIndex(index);
|
title: t('common.delete'),
|
||||||
try {
|
message: t('api_keys.delete_confirm'),
|
||||||
await apiKeysApi.delete(index);
|
variant: 'danger',
|
||||||
const nextKeys = apiKeys.filter((_, idx) => idx !== index);
|
onConfirm: async () => {
|
||||||
setApiKeys(nextKeys);
|
try {
|
||||||
updateConfigValue('api-keys', nextKeys);
|
await apiKeysApi.delete(index);
|
||||||
clearCache('api-keys');
|
const nextKeys = apiKeys.filter((_, idx) => idx !== index);
|
||||||
showNotification(t('notification.api_key_deleted'), 'success');
|
setApiKeys(nextKeys);
|
||||||
} catch (err: any) {
|
updateConfigValue('api-keys', nextKeys);
|
||||||
showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error');
|
clearCache('api-keys');
|
||||||
} finally {
|
showNotification(t('notification.api_key_deleted'), 'success');
|
||||||
setDeletingIndex(null);
|
} catch (err: any) {
|
||||||
}
|
showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionButtons = (
|
const actionButtons = (
|
||||||
@@ -181,8 +183,7 @@ export function ApiKeysPage() {
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleDelete(index)}
|
onClick={() => handleDelete(index)}
|
||||||
disabled={disableControls || deletingIndex === index}
|
disabled={disableControls}
|
||||||
loading={deletingIndex === index}
|
|
||||||
>
|
>
|
||||||
{t('common.delete')}
|
{t('common.delete')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -8,15 +8,38 @@ import type { Notification, NotificationType } from '@/types';
|
|||||||
import { generateId } from '@/utils/helpers';
|
import { generateId } from '@/utils/helpers';
|
||||||
import { NOTIFICATION_DURATION_MS } from '@/utils/constants';
|
import { NOTIFICATION_DURATION_MS } from '@/utils/constants';
|
||||||
|
|
||||||
|
interface ConfirmationOptions {
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
variant?: 'danger' | 'primary' | 'secondary';
|
||||||
|
onConfirm: () => void | Promise<void>;
|
||||||
|
onCancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface NotificationState {
|
interface NotificationState {
|
||||||
notifications: Notification[];
|
notifications: Notification[];
|
||||||
|
confirmation: {
|
||||||
|
isOpen: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
options: ConfirmationOptions | null;
|
||||||
|
};
|
||||||
showNotification: (message: string, type?: NotificationType, duration?: number) => void;
|
showNotification: (message: string, type?: NotificationType, duration?: number) => void;
|
||||||
removeNotification: (id: string) => void;
|
removeNotification: (id: string) => void;
|
||||||
clearAll: () => void;
|
clearAll: () => void;
|
||||||
|
showConfirmation: (options: ConfirmationOptions) => void;
|
||||||
|
hideConfirmation: () => void;
|
||||||
|
setConfirmationLoading: (loading: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useNotificationStore = create<NotificationState>((set) => ({
|
export const useNotificationStore = create<NotificationState>((set) => ({
|
||||||
notifications: [],
|
notifications: [],
|
||||||
|
confirmation: {
|
||||||
|
isOpen: false,
|
||||||
|
isLoading: false,
|
||||||
|
options: null
|
||||||
|
},
|
||||||
|
|
||||||
showNotification: (message, type = 'info', duration = NOTIFICATION_DURATION_MS) => {
|
showNotification: (message, type = 'info', duration = NOTIFICATION_DURATION_MS) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
@@ -49,5 +72,34 @@ export const useNotificationStore = create<NotificationState>((set) => ({
|
|||||||
|
|
||||||
clearAll: () => {
|
clearAll: () => {
|
||||||
set({ notifications: [] });
|
set({ notifications: [] });
|
||||||
|
},
|
||||||
|
|
||||||
|
showConfirmation: (options) => {
|
||||||
|
set({
|
||||||
|
confirmation: {
|
||||||
|
isOpen: true,
|
||||||
|
isLoading: false,
|
||||||
|
options
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
hideConfirmation: () => {
|
||||||
|
set((state) => ({
|
||||||
|
confirmation: {
|
||||||
|
...state.confirmation,
|
||||||
|
isOpen: false,
|
||||||
|
options: null // Cleanup
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
setConfirmationLoading: (loading) => {
|
||||||
|
set((state) => ({
|
||||||
|
confirmation: {
|
||||||
|
...state.confirmation,
|
||||||
|
isLoading: loading
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user