init commit

This commit is contained in:
iqudoo
2026-06-12 19:34:27 +08:00
commit 89b59ecc1b
38 changed files with 17719 additions and 0 deletions

View File

@@ -0,0 +1,204 @@
import React, {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { AlertCircle, CheckCircle2, Info, X } from 'lucide-react';
export type AppDialogVariant = 'info' | 'success' | 'warning' | 'error';
interface AlertOptions {
title?: string;
variant?: AppDialogVariant;
confirmLabel?: string;
}
interface ConfirmOptions extends AlertOptions {
cancelLabel?: string;
}
interface DialogRequest {
mode: 'alert' | 'confirm';
message: string;
title: string;
variant: AppDialogVariant;
confirmLabel: string;
cancelLabel: string;
resolve: (value: boolean) => void;
}
interface AppDialogContextValue {
showAlert: (message: string, options?: AlertOptions) => Promise<void>;
showConfirm: (message: string, options?: ConfirmOptions) => Promise<boolean>;
}
const AppDialogContext = createContext<AppDialogContextValue | null>(null);
const VARIANT_META: Record<
AppDialogVariant,
{ icon: React.ReactNode; title: string; buttonClass: string }
> = {
info: {
icon: <Info className="w-5 h-5 text-indigo-500" />,
title: '提示',
buttonClass: 'bg-indigo-600 hover:bg-indigo-700 text-white',
},
success: {
icon: <CheckCircle2 className="w-5 h-5 text-emerald-500" />,
title: '成功',
buttonClass: 'bg-emerald-600 hover:bg-emerald-700 text-white',
},
warning: {
icon: <AlertCircle className="w-5 h-5 text-amber-500" />,
title: '请注意',
buttonClass: 'bg-amber-500 hover:bg-amber-600 text-white',
},
error: {
icon: <AlertCircle className="w-5 h-5 text-rose-500" />,
title: '出错了',
buttonClass: 'bg-rose-600 hover:bg-rose-700 text-white',
},
};
function AppDialogModal({
dialog,
onClose,
}: {
dialog: DialogRequest;
onClose: (confirmed: boolean) => void;
}) {
const confirmBtnRef = useRef<HTMLButtonElement>(null);
const meta = VARIANT_META[dialog.variant];
useEffect(() => {
confirmBtnRef.current?.focus();
}, []);
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
onClose(dialog.mode === 'alert');
}
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [dialog.mode, onClose]);
return (
<div
className="fixed inset-0 z-[9500] flex items-center justify-center bg-slate-950/60 backdrop-blur-sm p-4"
onClick={() => onClose(dialog.mode === 'alert')}
role="presentation"
>
<div
className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden"
onClick={(e) => e.stopPropagation()}
role="alertdialog"
aria-modal="true"
aria-labelledby="app-dialog-title"
aria-describedby="app-dialog-message"
>
<div className="flex items-start gap-3 px-5 py-4 border-b border-gray-100">
<span className="shrink-0 mt-0.5">{meta.icon}</span>
<div className="flex-1 min-w-0 pr-2">
<h3 id="app-dialog-title" className="text-sm font-bold text-gray-900">
{dialog.title}
</h3>
</div>
<button
type="button"
onClick={() => onClose(dialog.mode === 'alert')}
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition cursor-pointer shrink-0"
aria-label="关闭"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="px-5 py-4">
<p id="app-dialog-message" className="text-sm text-gray-600 leading-relaxed whitespace-pre-wrap">
{dialog.message}
</p>
</div>
<div className="flex justify-end gap-2 px-5 py-4 border-t border-gray-100 bg-gray-50/80">
{dialog.mode === 'confirm' && (
<button
type="button"
onClick={() => onClose(false)}
className="px-4 py-2 border border-gray-200 text-gray-600 hover:bg-white rounded-lg text-sm font-medium cursor-pointer"
>
{dialog.cancelLabel}
</button>
)}
<button
ref={confirmBtnRef}
type="button"
onClick={() => onClose(true)}
className={`px-4 py-2 rounded-lg text-sm font-semibold cursor-pointer ${meta.buttonClass}`}
>
{dialog.confirmLabel}
</button>
</div>
</div>
</div>
);
}
export function AppDialogProvider({ children }: { children: React.ReactNode }) {
const [dialog, setDialog] = useState<DialogRequest | null>(null);
const showAlert = useCallback((message: string, options?: AlertOptions) => {
const variant = options?.variant ?? 'info';
return new Promise<void>((resolve) => {
setDialog({
mode: 'alert',
message,
title: options?.title ?? VARIANT_META[variant].title,
variant,
confirmLabel: options?.confirmLabel ?? '知道了',
cancelLabel: '',
resolve: () => resolve(),
});
});
}, []);
const showConfirm = useCallback((message: string, options?: ConfirmOptions) => {
const variant = options?.variant ?? 'warning';
return new Promise<boolean>((resolve) => {
setDialog({
mode: 'confirm',
message,
title: options?.title ?? VARIANT_META[variant].title,
variant,
confirmLabel: options?.confirmLabel ?? '确定',
cancelLabel: options?.cancelLabel ?? '取消',
resolve,
});
});
}, []);
const closeDialog = useCallback((confirmed: boolean) => {
setDialog((current) => {
current?.resolve(confirmed);
return null;
});
}, []);
return (
<AppDialogContext.Provider value={{ showAlert, showConfirm }}>
{children}
{dialog && <AppDialogModal dialog={dialog} onClose={closeDialog} />}
</AppDialogContext.Provider>
);
}
export function useAppDialog() {
const ctx = useContext(AppDialogContext);
if (!ctx) {
throw new Error('useAppDialog must be used within AppDialogProvider');
}
return ctx;
}