优化布局和交互
This commit is contained in:
@@ -33,6 +33,7 @@ interface DialogRequest {
|
||||
interface AppDialogContextValue {
|
||||
showAlert: (message: string, options?: AlertOptions) => Promise<void>;
|
||||
showConfirm: (message: string, options?: ConfirmOptions) => Promise<boolean>;
|
||||
showToast: (message: string, options?: { variant?: AppDialogVariant; duration?: number }) => void;
|
||||
}
|
||||
|
||||
const AppDialogContext = createContext<AppDialogContextValue | null>(null);
|
||||
@@ -147,8 +148,40 @@ function AppDialogModal({
|
||||
);
|
||||
}
|
||||
|
||||
function AppToast({
|
||||
message,
|
||||
variant = 'success',
|
||||
onDismiss,
|
||||
}: {
|
||||
message: string;
|
||||
variant: AppDialogVariant;
|
||||
onDismiss: () => void;
|
||||
}) {
|
||||
const meta = VARIANT_META[variant];
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(onDismiss, 2000);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [onDismiss]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed left-1/2 bottom-[max(1.25rem,env(safe-area-inset-bottom))] z-[10001] -translate-x-1/2 pointer-events-none"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div className="flex items-center gap-2 px-4 py-2.5 rounded-xl shadow-lg border border-black/10 bg-[#2a2a2a] text-[#f0f0f0] text-[12px] font-medium max-w-[min(90vw,20rem)]">
|
||||
<span className="shrink-0">{meta.icon}</span>
|
||||
<span className="truncate">{message}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppDialogProvider({ children }: { children: React.ReactNode }) {
|
||||
const [dialog, setDialog] = useState<DialogRequest | null>(null);
|
||||
const [toast, setToast] = useState<{ message: string; variant: AppDialogVariant } | null>(null);
|
||||
const toastTimerRef = useRef<number | null>(null);
|
||||
|
||||
const showAlert = useCallback((message: string, options?: AlertOptions) => {
|
||||
const variant = options?.variant ?? 'info';
|
||||
@@ -180,6 +213,28 @@ export function AppDialogProvider({ children }: { children: React.ReactNode }) {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const dismissToast = useCallback(() => {
|
||||
if (toastTimerRef.current !== null) {
|
||||
window.clearTimeout(toastTimerRef.current);
|
||||
toastTimerRef.current = null;
|
||||
}
|
||||
setToast(null);
|
||||
}, []);
|
||||
|
||||
const showToast = useCallback(
|
||||
(message: string, options?: { variant?: AppDialogVariant; duration?: number }) => {
|
||||
dismissToast();
|
||||
const variant = options?.variant ?? 'success';
|
||||
setToast({ message, variant });
|
||||
const duration = options?.duration ?? 2000;
|
||||
toastTimerRef.current = window.setTimeout(() => {
|
||||
toastTimerRef.current = null;
|
||||
setToast(null);
|
||||
}, duration);
|
||||
},
|
||||
[dismissToast]
|
||||
);
|
||||
|
||||
const closeDialog = useCallback((confirmed: boolean) => {
|
||||
setDialog((current) => {
|
||||
current?.resolve(confirmed);
|
||||
@@ -188,9 +243,12 @@ export function AppDialogProvider({ children }: { children: React.ReactNode }) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AppDialogContext.Provider value={{ showAlert, showConfirm }}>
|
||||
<AppDialogContext.Provider value={{ showAlert, showConfirm, showToast }}>
|
||||
{children}
|
||||
{dialog && <AppDialogModal dialog={dialog} onClose={closeDialog} />}
|
||||
{toast && (
|
||||
<AppToast message={toast.message} variant={toast.variant} onDismiss={dismissToast} />
|
||||
)}
|
||||
</AppDialogContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user