mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-12 02:06:37 +08:00
[新增] 集成 Sonner 通知库,添加 Toaster 组件并在 InlineMessage 中使用
This commit is contained in:
@@ -1,35 +1,44 @@
|
|||||||
import { cn } from '@/lib/utils/cn';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
type InlineMessageTone = 'info' | 'success' | 'danger';
|
type InlineMessageTone = 'info' | 'success' | 'danger';
|
||||||
|
|
||||||
const toneClasses: Record<InlineMessageTone, string> = {
|
|
||||||
info: 'border-[var(--status-info-border)] bg-[var(--status-info-soft)] text-[var(--status-info-foreground)]',
|
|
||||||
success:
|
|
||||||
'border-[var(--status-success-border)] bg-[var(--status-success-soft)] text-[var(--status-success-foreground)]',
|
|
||||||
danger:
|
|
||||||
'border-[var(--status-danger-border)] bg-[var(--status-danger-soft)] text-[var(--status-danger-foreground)]',
|
|
||||||
};
|
|
||||||
|
|
||||||
interface InlineMessageProps {
|
interface InlineMessageProps {
|
||||||
tone?: InlineMessageTone;
|
tone?: InlineMessageTone;
|
||||||
message: string;
|
message: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onClear?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InlineMessage({
|
export function InlineMessage({
|
||||||
tone = 'info',
|
tone = 'info',
|
||||||
message,
|
message,
|
||||||
className,
|
onClear,
|
||||||
}: InlineMessageProps) {
|
}: InlineMessageProps) {
|
||||||
return (
|
useEffect(() => {
|
||||||
<div
|
if (!message) return;
|
||||||
className={cn(
|
|
||||||
'rounded-2xl border px-4 py-3 text-sm leading-6',
|
const options = {
|
||||||
toneClasses[tone],
|
position: 'bottom-right' as const,
|
||||||
className,
|
};
|
||||||
)}
|
|
||||||
>
|
if (tone === 'success') {
|
||||||
{message}
|
toast.success(message, options);
|
||||||
</div>
|
} else if (tone === 'danger') {
|
||||||
);
|
toast.error(message, options);
|
||||||
|
} else {
|
||||||
|
toast(message, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onClear) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
onClear();
|
||||||
|
}, 0);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [tone, message, onClear]);
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { type ReactNode, useState } from 'react';
|
|||||||
|
|
||||||
import { AuthProvider } from '@/components/providers/auth-provider';
|
import { AuthProvider } from '@/components/providers/auth-provider';
|
||||||
import { ThemeProvider } from '@/components/providers/theme-provider';
|
import { ThemeProvider } from '@/components/providers/theme-provider';
|
||||||
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
|
|
||||||
interface AppProvidersProps {
|
interface AppProvidersProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -30,7 +31,10 @@ export function AppProviders({ children }: AppProvidersProps) {
|
|||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AuthProvider>{children}</AuthProvider>
|
<AuthProvider>
|
||||||
|
{children}
|
||||||
|
<Toaster />
|
||||||
|
</AuthProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useTheme } from '@/components/providers/theme-provider';
|
||||||
|
import { Toaster as Sonner } from 'sonner';
|
||||||
|
|
||||||
|
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||||
|
|
||||||
|
export function Toaster({ ...props }: ToasterProps) {
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={resolvedTheme as ToasterProps['theme']}
|
||||||
|
className="toaster group"
|
||||||
|
toastOptions={{
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
'group toast group-[.toaster]:bg-[var(--surface-elevated)] group-[.toaster]:text-[var(--foreground-primary)] group-[.toaster]:border-[var(--border-default)] group-[.toaster]:shadow-lg group-[.toaster]:rounded-2xl group-[.toaster]:font-sans',
|
||||||
|
description: 'group-[.toast]:text-[var(--foreground-secondary)]',
|
||||||
|
actionButton:
|
||||||
|
'group-[.toast]:bg-[var(--primary)] group-[.toast]:text-[var(--primary-foreground)]',
|
||||||
|
cancelButton:
|
||||||
|
'group-[.toast]:bg-[var(--muted)] group-[.toast]:text-[var(--muted-foreground)]',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -208,7 +208,11 @@ export function ApplyLogsPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{feedback ? (
|
{feedback ? (
|
||||||
<InlineMessage tone={feedback.tone} message={feedback.message} />
|
<InlineMessage
|
||||||
|
tone={feedback.tone}
|
||||||
|
message={feedback.message}
|
||||||
|
onClear={() => setFeedback(null)}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<AppCard
|
<AppCard
|
||||||
|
|||||||
@@ -547,7 +547,11 @@ export function ConfigVersionsPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{feedback ? (
|
{feedback ? (
|
||||||
<InlineMessage tone={feedback.tone} message={feedback.message} />
|
<InlineMessage
|
||||||
|
tone={feedback.tone}
|
||||||
|
message={feedback.message}
|
||||||
|
onClear={() => setFeedback(null)}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{publishPreview ? (
|
{publishPreview ? (
|
||||||
|
|||||||
@@ -90,7 +90,11 @@ export function DnsAccountsPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{feedback ? (
|
{feedback ? (
|
||||||
<InlineMessage tone={feedback.tone} message={feedback.message} />
|
<InlineMessage
|
||||||
|
tone={feedback.tone}
|
||||||
|
message={feedback.message}
|
||||||
|
onClear={() => setFeedback(null)}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<AppCard title="DNS 账号列表">
|
<AppCard title="DNS 账号列表">
|
||||||
|
|||||||
@@ -702,7 +702,11 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{feedback ? (
|
{feedback ? (
|
||||||
<InlineMessage tone={feedback.tone} message={feedback.message} />
|
<InlineMessage
|
||||||
|
tone={feedback.tone}
|
||||||
|
message={feedback.message}
|
||||||
|
onClear={() => setFeedback(null)}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
|
|||||||
@@ -207,7 +207,11 @@ export function NodesPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{feedback ? (
|
{feedback ? (
|
||||||
<InlineMessage tone={feedback.tone} message={feedback.message} />
|
<InlineMessage
|
||||||
|
tone={feedback.tone}
|
||||||
|
message={feedback.message}
|
||||||
|
onClear={() => setFeedback(null)}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<AppCard
|
<AppCard
|
||||||
|
|||||||
@@ -83,7 +83,11 @@ export function OriginsPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{feedback ? (
|
{feedback ? (
|
||||||
<InlineMessage tone={feedback.tone} message={feedback.message} />
|
<InlineMessage
|
||||||
|
tone={feedback.tone}
|
||||||
|
message={feedback.message}
|
||||||
|
onClear={() => setFeedback(null)}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<AppCard
|
<AppCard
|
||||||
|
|||||||
@@ -612,7 +612,11 @@ export function PerformancePage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{feedback ? (
|
{feedback ? (
|
||||||
<InlineMessage tone={feedback.tone} message={feedback.message} />
|
<InlineMessage
|
||||||
|
tone={feedback.tone}
|
||||||
|
message={feedback.message}
|
||||||
|
onClear={() => setFeedback(null)}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
|
|||||||
@@ -220,7 +220,11 @@ export function ProxyRoutesPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{feedback ? (
|
{feedback ? (
|
||||||
<InlineMessage tone={feedback.tone} message={feedback.message} />
|
<InlineMessage
|
||||||
|
tone={feedback.tone}
|
||||||
|
message={feedback.message}
|
||||||
|
onClear={() => setFeedback(null)}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="grid gap-4 xl:grid-cols-4">
|
<div className="grid gap-4 xl:grid-cols-4">
|
||||||
|
|||||||
@@ -323,7 +323,11 @@ export function WAFIPGroupsPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{feedback ? (
|
{feedback ? (
|
||||||
<InlineMessage tone={feedback.tone} message={feedback.message} />
|
<InlineMessage
|
||||||
|
tone={feedback.tone}
|
||||||
|
message={feedback.message}
|
||||||
|
onClear={() => setFeedback(null)}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="grid gap-5 xl:grid-cols-[360px_minmax(0,1fr)]">
|
<div className="grid gap-5 xl:grid-cols-[360px_minmax(0,1fr)]">
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"shadcn": "^4.1.0",
|
"shadcn": "^4.1.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
|
|||||||
Generated
+1640
-4761
File diff suppressed because it is too large
Load Diff
@@ -1 +1,26 @@
|
|||||||
import '@testing-library/jest-dom/vitest';
|
import '@testing-library/jest-dom/vitest';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
const toastMock = vi.fn((msg) => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.textContent = typeof msg === 'string' ? msg : '';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
return 'toast-id';
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('sonner', () => ({
|
||||||
|
toast: Object.assign(toastMock, {
|
||||||
|
success: vi.fn((msg) => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.textContent = typeof msg === 'string' ? msg : '';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
return 'toast-id';
|
||||||
|
}),
|
||||||
|
error: vi.fn((msg) => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.textContent = typeof msg === 'string' ? msg : '';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
return 'toast-id';
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user