diff --git a/api/errorHandle.ts b/api/errorHandle.ts index 50b7fa7..2486b2d 100644 --- a/api/errorHandle.ts +++ b/api/errorHandle.ts @@ -1,4 +1,3 @@ -import { message } from "antd"; import { debounce } from "lodash"; /** @@ -52,11 +51,7 @@ export const errorHandle = debounce( customMessage || HTTP_ERROR_MESSAGES[code] || DEFAULT_ERROR_MESSAGE; // 显示错误提示 - message.error({ - content: errorMessage, - duration: 3, - className: 'custom-error-message' - }); + window.msg.error(errorMessage); // 执行特殊错误码的处理函数 const handler = ERROR_HANDLERS[code]; diff --git a/api/request.ts b/api/request.ts index 7a59dbf..c30cc5a 100644 --- a/api/request.ts +++ b/api/request.ts @@ -1,5 +1,4 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, AxiosHeaders } from 'axios'; -import { message } from "antd"; import { BASE_URL } from './constants' import { errorHandle } from './errorHandle'; diff --git a/app/page.tsx b/app/page.tsx index e2c41ee..782e582 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,13 +1,11 @@ import { TopBar } from "@/components/layout/top-bar"; import { HomePage2 } from "@/components/pages/home-page2"; -import OAuthCallbackHandler from "@/components/ui/oauth-callback-handler"; export default function Home() { return ( <> - > ); diff --git a/app/service/Interaction/MovieProjectService.ts b/app/service/Interaction/MovieProjectService.ts index 1d4ebb0..428bd44 100644 --- a/app/service/Interaction/MovieProjectService.ts +++ b/app/service/Interaction/MovieProjectService.ts @@ -1,7 +1,6 @@ -import { CreateMovieProjectV2Request, CreateMovieProjectV3Request } from "@/api/DTO/movie_start_dto"; + import { createMovieProject, createMovieProjectV2, createMovieProjectV3 } from "@/api/create_movie"; -import { QueueResponse, QueueStatus, withQueuePolling, QueueResponseData } from "@/api/movie_queue"; -import { message } from "antd"; +import { QueueResponse, withQueuePolling, QueueResponseData } from "@/api/movie_queue"; /** * 电影项目创建模式 @@ -56,13 +55,13 @@ export class MovieProjectService { }, onError: (error: Error) => { if (error.message === '操作已取消') { - message.info('Queue cancelled'); + window.msg.info('Queue cancelled'); } else { - message.error(error instanceof Error ? error.message : "Failed to create project"); + window.msg.error(error instanceof Error ? error.message : "Failed to create project"); } }, onCancel: () => { - message.info('Queue cancelled'); + window.msg.info('Queue cancelled'); } }); @@ -80,7 +79,7 @@ export class MovieProjectService { if (error instanceof Error && error.message === '操作已取消') { throw error; } - message.error(error instanceof Error ? error.message : "Failed to create project"); + window.msg.error(error instanceof Error ? error.message : "Failed to create project"); throw error; } } diff --git a/app/service/Interaction/templateStoryService.ts b/app/service/Interaction/templateStoryService.ts index 99f8a13..89dacc4 100644 --- a/app/service/Interaction/templateStoryService.ts +++ b/app/service/Interaction/templateStoryService.ts @@ -1,4 +1,3 @@ -import { message } from "antd"; import { StoryTemplateEntity } from "../domain/Entities"; import { useUploadFile } from "../domain/service"; import { debounce } from "lodash"; diff --git a/app/types/global.d.ts b/app/types/global.d.ts new file mode 100644 index 0000000..6136c85 --- /dev/null +++ b/app/types/global.d.ts @@ -0,0 +1,16 @@ +import { GlobalMessage } from '@/components/common/GlobalMessage'; +import { toast } from 'sonner'; + +declare global { + interface Window { + msg: GlobalMessage; + $message: { + success: (message: string, duration?: number) => void; + error: (message: string, duration?: number) => void; + warning: (message: string, duration?: number) => void; + info: (message: string, duration?: number) => void; + loading: (message: string) => ReturnType; + dismiss: () => void; + }; + } +} \ No newline at end of file diff --git a/components/common/GlobalMessage.tsx b/components/common/GlobalMessage.tsx new file mode 100644 index 0000000..a31c859 --- /dev/null +++ b/components/common/GlobalMessage.tsx @@ -0,0 +1,92 @@ +'use client'; + +import { toast, type ToastT } from 'sonner'; +import { + CheckCircle2, + XCircle, + AlertCircle, + Info, + Loader2 +} from 'lucide-react'; + +/** + * 全局消息提示工具 + * 对 sonner toast 进行封装,提供更简洁的 API + */ +class GlobalMessage { + success(message: string, duration = 3000, options?: Partial) { + toast.success(message, { + icon: , + duration, + ...options, + }); + } + + error(message: string, duration = 3000, options?: Partial) { + toast.error(message, { + icon: , + duration, + ...options, + }); + } + + warning(message: string, duration = 3000, options?: Partial) { + toast.warning(message, { + icon: , + duration, + ...options, + }); + } + + info(message: string, duration = 3000, options?: Partial) { + toast(message, { + icon: , + duration, + ...options, + }); + } + + loading(message: string, options?: Partial) { + return toast.promise( + new Promise(() => {}), + { + loading: message, + icon: , + ...options, + } + ); + } + + dismiss() { + toast.dismiss(); + } +} + +// 导出单例实例 +export const msg = new GlobalMessage(); + +// 为了方便使用,也导出单独的方法 +export const { success, error, warning, info, loading, dismiss } = msg; +// 在文件末尾添加全局注册方法 +export function registerGlobalMessage() { + if (typeof window !== 'undefined') { + // 注册完整实例 + window.msg = msg; + + // 注册便捷方法 + window.$message = { + success: msg.success.bind(msg), + error: msg.error.bind(msg), + warning: msg.warning.bind(msg), + info: msg.info.bind(msg), + loading: msg.loading.bind(msg), + dismiss: msg.dismiss.bind(msg), + }; + + // 添加调试信息 + console.log('GlobalMessage registered:', { + msg: window.msg, + $message: window.$message + }); + } +} diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx index 1f78b40..0be548b 100644 --- a/components/layout/top-bar.tsx +++ b/components/layout/top-bar.tsx @@ -239,9 +239,9 @@ export function TopBar({ collapsed }: { collapsed: boolean }) { )} {/* Notifications */} - {/* showQueueNotification(3, 10)}> + window.msg.error('Loading...')}> - */} + {/* Theme Toggle */} {/* - + - Loading more projects... )} )} + + {episodeList.length === 0 && isLoading && ( + + + + + + )} {/* 视频工具组件 - 使用独立组件 */} diff --git a/components/pages/work-flow/use-workflow-data.tsx b/components/pages/work-flow/use-workflow-data.tsx index a83c883..766eb0d 100644 --- a/components/pages/work-flow/use-workflow-data.tsx +++ b/components/pages/work-flow/use-workflow-data.tsx @@ -8,7 +8,6 @@ import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovi import { useScriptService } from "@/app/service/Interaction/ScriptService"; import { useUpdateEffect } from '@/app/hooks/useUpdateEffect'; import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit'; -import { msg } from '@/utils/message'; interface UseWorkflowDataProps { onEditPlanGenerated?: () => void; @@ -209,7 +208,7 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = useEffect(() => { if (isShowError) { - msg.error('Too many failed storyboards, unable to execute automatic editing.', 3000); + window.msg.error('Too many failed storyboards, unable to execute automatic editing.', 3000); } }, [isShowError]); diff --git a/components/providers.tsx b/components/providers.tsx index f6a6618..fc89d3f 100644 --- a/components/providers.tsx +++ b/components/providers.tsx @@ -3,15 +3,11 @@ import { Provider } from 'react-redux'; import { store } from '@/lib/store/store'; import { ThemeProvider } from 'next-themes'; -import { Toaster } from '@/components/ui/sonner'; +import { Toaster } from 'sonner'; import AuthGuard from './auth/auth-guard'; import dynamic from 'next/dynamic'; - -// 动态导入 OAuthCallbackHandler 和 DevHelper -const OAuthCallbackHandler = dynamic( - () => import('./ui/oauth-callback-handler').then(mod => mod.default), - { ssr: false } -); +import { registerGlobalMessage } from '@/components/common/GlobalMessage'; +import { useEffect } from 'react'; const DevHelper = dynamic( () => import('@/utils/dev-helper').then(mod => (mod as any).default), @@ -19,6 +15,11 @@ const DevHelper = dynamic( ); export function Providers({ children }: { children: React.ReactNode }) { + // 注册全局消息提醒 + useEffect(() => { + registerGlobalMessage(); + }, []); + return ( {children} - - + {process.env.NODE_ENV === 'development' && } diff --git a/components/script-renderer/ScriptRenderer.tsx b/components/script-renderer/ScriptRenderer.tsx index 60ef418..f859a1b 100644 --- a/components/script-renderer/ScriptRenderer.tsx +++ b/components/script-renderer/ScriptRenderer.tsx @@ -5,7 +5,6 @@ import { ScriptData, ScriptBlock, ScriptContent, ThemeTagBgColor, ThemeType } fr import ContentEditable, { ContentEditableEvent } from 'react-contenteditable'; import { SelectDropdown } from '@/components/ui/select-dropdown'; import { TypewriterText } from '@/components/workflow/work-office/common/TypewriterText'; -import { msg } from '@/utils/message'; interface ScriptRendererProps { data: any[]; @@ -126,7 +125,7 @@ export const ScriptRenderer: React.FC = ({ data, setIsPause const handleThemeTagChange = (value: string[]) => { console.log('主题标签更改', value); if (value.length > 5) { - msg.error('最多可选择5个主题标签', 3000); + window.msg.error('max 5 theme tags', 3000); return; } setAddThemeTag(value); @@ -217,7 +216,7 @@ export const ScriptRenderer: React.FC = ({ data, setIsPause className="w-6 h-6 p-1 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors" onClick={() => { // 提示权限不够 - msg.error('No permission!'); + window.msg.error('No permission!'); return; handleEditBlock(block); }} @@ -226,7 +225,7 @@ export const ScriptRenderer: React.FC = ({ data, setIsPause className="w-6 h-6 p-1 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors" onClick={() => { navigator.clipboard.writeText(block.content.map(item => item.text).join('\n')); - msg.success('Copied!'); + window.msg.success('Copied!'); }} /> diff --git a/components/ui/character-tab-content.tsx b/components/ui/character-tab-content.tsx index a725d2f..040e154 100644 --- a/components/ui/character-tab-content.tsx +++ b/components/ui/character-tab-content.tsx @@ -12,7 +12,6 @@ import { useEditData } from '@/components/pages/work-flow/use-edit-data'; import { useSearchParams } from 'next/navigation'; import { RoleEntity } from '@/app/service/domain/Entities'; import { Role } from '@/api/DTO/movieEdit'; -import { msg } from '@/utils/message'; interface CharacterTabContentProps { originalRoles: Role[]; @@ -113,7 +112,7 @@ CharacterTabContentProps const handleSmartPolish = (text: string) => { // 然后调用优化角色文本 - msg.error('No permission!'); + window.msg.error('No permission!'); return; optimizeRoleText(text); }; @@ -212,7 +211,7 @@ CharacterTabContentProps }; const handleOpenReplaceLibrary = async () => { - msg.error('No permission!'); + window.msg.error('No permission!'); return; setIsLoadingLibrary(true); setIsReplaceLibraryOpen(true); @@ -222,7 +221,7 @@ CharacterTabContentProps }; const handleRegenerate = async () => { - msg.error('No permission!'); + window.msg.error('No permission!'); return; console.log('Regenerate'); setIsRegenerate(true); @@ -237,7 +236,7 @@ CharacterTabContentProps }; const handleUploadClick = () => { - msg.error('No permission!'); + window.msg.error('No permission!'); return; fileInputRef.current?.click(); }; diff --git a/components/ui/edit-modal.tsx b/components/ui/edit-modal.tsx index 0514a4c..00cdb79 100644 --- a/components/ui/edit-modal.tsx +++ b/components/ui/edit-modal.tsx @@ -13,7 +13,6 @@ import { MusicTabContent } from './music-tab-content'; import FloatingGlassPanel from './FloatingGlassPanel'; import { SaveEditUseCase } from '@/app/service/usecase/SaveEditUseCase'; import { TaskObject } from '@/api/DTO/movieEdit'; -import { msg } from '@/utils/message'; interface EditModalProps { isOpen: boolean; @@ -124,7 +123,7 @@ export function EditModal({ } const handleSave = () => { - msg.error('No permission!'); + window.msg.error('No permission!'); return; console.log('handleSave'); // setIsRemindFallbackOpen(true); @@ -143,7 +142,7 @@ export function EditModal({ } const handleConfirmGotoFallback = () => { - msg.error('No permission!'); + window.msg.error('No permission!'); return; setDisabledBtn(true); console.log('handleConfirmGotoFallback'); @@ -170,7 +169,7 @@ export function EditModal({ } const handleReset = () => { - msg.error('No permission!'); + window.msg.error('No permission!'); return; console.log('handleReset'); // 重置当前tab修改的数据 diff --git a/components/ui/oauth-callback-handler.tsx b/components/ui/oauth-callback-handler.tsx deleted file mode 100644 index d11e4a1..0000000 --- a/components/ui/oauth-callback-handler.tsx +++ /dev/null @@ -1,90 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { validateOAuthState } from '@/lib/auth'; -import { toast } from '@/hooks/use-toast'; - -export default function OAuthCallbackHandler() { - const searchParams = useSearchParams(); - const router = useRouter(); - - useEffect(() => { - // Check if this is an OAuth callback - const state = searchParams.get('state'); - const session = searchParams.get('session'); - const userJson = searchParams.get('user'); - const error = searchParams.get('error'); - - // Log debug information - console.log('OAuth callback handler running with params:', { - state: state || 'not present', - session: session ? 'present' : 'not present', - userJson: userJson ? 'present' : 'not present', - error: error || 'none' - }); - - // Handle error case - if (error) { - console.error('OAuth error:', error); - toast({ - title: 'Authentication Error', - description: `Google sign-in failed: ${error}`, - variant: 'destructive', - }); - router.push(`/login?error=${error}`); - return; - } - - // If we have state and session, this might be an OAuth callback - if (state && session) { - // Validate the state parameter to prevent CSRF - const isValid = validateOAuthState(state); - - if (!isValid) { - // State validation failed, possible CSRF attack - console.error('OAuth state validation failed'); - toast({ - title: 'Authentication Error', - description: 'Security validation failed. Please try signing in again.', - variant: 'destructive', - }); - router.push('/login?error=invalid_state'); - return; - } - - console.log('OAuth state validation successful'); - - // State is valid, process the login - if (userJson) { - try { - const user = JSON.parse(decodeURIComponent(userJson)); - console.log('OAuth user data parsed successfully'); - - // Store the user in session - sessionStorage.setItem('currentUser', JSON.stringify(user)); - - // Show success message - toast({ - title: 'Signed in successfully', - description: `Welcome ${user.name}!`, - }); - - // Remove the query parameters from the URL - router.replace('/'); - } catch (error) { - console.error('Failed to parse user data', error); - toast({ - title: 'Authentication Error', - description: 'Failed to process authentication data', - variant: 'destructive', - }); - router.push('/login?error=invalid_user_data'); - } - } - } - }, [searchParams, router]); - - // This is a utility component that doesn't render anything - return null; -} \ No newline at end of file diff --git a/components/ui/shot-editor/ShotsEditor.tsx b/components/ui/shot-editor/ShotsEditor.tsx index a670393..cf8d7ec 100644 --- a/components/ui/shot-editor/ShotsEditor.tsx +++ b/components/ui/shot-editor/ShotsEditor.tsx @@ -2,7 +2,6 @@ import React, { forwardRef, useRef, useState } from "react"; import { useDeepCompareEffect } from "@/hooks/useDeepCompareEffect"; import { Plus, X, UserRoundPlus, MessageCirclePlus, MessageCircleMore, ClipboardType } from "lucide-react"; import ShotEditor from "./ShotEditor"; -import { toast } from "sonner"; import { TextToShotAdapter } from "@/app/service/adapter/textToShot"; @@ -90,11 +89,7 @@ export const ShotsEditor = forwardRef(function ShotsEdito const addShot = () => { if (shots.length > 3) { - toast.error('No more than 4 shots', { - duration: 3000, - position: 'top-center', - richColors: true, - }); + window.msg.error('max 4 shots'); return; } const newShot = createEmptyShot(); diff --git a/components/ui/shot-tab-content.tsx b/components/ui/shot-tab-content.tsx index 8ffc08c..2e3ba85 100644 --- a/components/ui/shot-tab-content.tsx +++ b/components/ui/shot-tab-content.tsx @@ -13,7 +13,6 @@ import HorizontalScroller from './HorizontalScroller'; import { useEditData } from '@/components/pages/work-flow/use-edit-data'; import { RoleEntity, VideoSegmentEntity } from '@/app/service/domain/Entities'; import { ShotVideo } from '@/api/DTO/movieEdit'; -import { msg } from '@/utils/message'; interface ShotTabContentProps { currentSketchIndex: number; @@ -448,7 +447,7 @@ export const ShotTabContent = forwardRef< {/* 人物替换按钮 */} { - msg.error('No permission!'); + window.msg.error('No permission!'); return; handleScan() }} @@ -504,7 +503,7 @@ export const ShotTabContent = forwardRef< */} { - msg.error('No permission!'); + window.msg.error('No permission!'); return; handleRegenerate(); }} diff --git a/components/ui/sonner.tsx b/components/ui/sonner.tsx deleted file mode 100644 index b38ad1e..0000000 --- a/components/ui/sonner.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import { useTheme } from 'next-themes'; -import { Toaster as Sonner } from 'sonner'; - -type ToasterProps = React.ComponentProps; - -const Toaster = ({ ...props }: ToasterProps) => { - const { theme = 'system' } = useTheme(); - - return ( - - ); -}; - -export { Toaster }; diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx deleted file mode 100644 index f410d8a..0000000 --- a/components/ui/toast.tsx +++ /dev/null @@ -1,129 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as ToastPrimitives from '@radix-ui/react-toast'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { X } from 'lucide-react'; - -import { cn } from '@/public/lib/utils'; - -const ToastProvider = ToastPrimitives.Provider; - -const ToastViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastViewport.displayName = ToastPrimitives.Viewport.displayName; - -const toastVariants = cva( - 'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full', - { - variants: { - variant: { - default: 'border bg-background text-foreground', - destructive: - 'destructive group border-destructive bg-destructive text-destructive-foreground', - }, - }, - defaultVariants: { - variant: 'default', - }, - } -); - -const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, variant, ...props }, ref) => { - return ( - - ); -}); -Toast.displayName = ToastPrimitives.Root.displayName; - -const ToastAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastAction.displayName = ToastPrimitives.Action.displayName; - -const ToastClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)); -ToastClose.displayName = ToastPrimitives.Close.displayName; - -const ToastTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastTitle.displayName = ToastPrimitives.Title.displayName; - -const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastDescription.displayName = ToastPrimitives.Description.displayName; - -type ToastProps = React.ComponentPropsWithoutRef; - -type ToastActionElement = React.ReactElement; - -export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction, -}; diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx deleted file mode 100644 index 8705cd8..0000000 --- a/components/ui/toaster.tsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client'; - -import { useToast } from '@/hooks/use-toast'; -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from '@/components/ui/toast'; - -export function Toaster() { - const { toasts } = useToast(); - - return ( - - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - - - {title && {title}} - {description && ( - {description} - )} - - {action} - - - ); - })} - - - ); -} diff --git a/hooks/use-toast.ts b/hooks/use-toast.ts deleted file mode 100644 index 7337115..0000000 --- a/hooks/use-toast.ts +++ /dev/null @@ -1,191 +0,0 @@ -'use client'; - -// Inspired by react-hot-toast library -import * as React from 'react'; - -import type { ToastActionElement, ToastProps } from '@/components/ui/toast'; - -const TOAST_LIMIT = 1; -const TOAST_REMOVE_DELAY = 1000000; - -type ToasterToast = ToastProps & { - id: string; - title?: React.ReactNode; - description?: React.ReactNode; - action?: ToastActionElement; -}; - -const actionTypes = { - ADD_TOAST: 'ADD_TOAST', - UPDATE_TOAST: 'UPDATE_TOAST', - DISMISS_TOAST: 'DISMISS_TOAST', - REMOVE_TOAST: 'REMOVE_TOAST', -} as const; - -let count = 0; - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER; - return count.toString(); -} - -type ActionType = typeof actionTypes; - -type Action = - | { - type: ActionType['ADD_TOAST']; - toast: ToasterToast; - } - | { - type: ActionType['UPDATE_TOAST']; - toast: Partial; - } - | { - type: ActionType['DISMISS_TOAST']; - toastId?: ToasterToast['id']; - } - | { - type: ActionType['REMOVE_TOAST']; - toastId?: ToasterToast['id']; - }; - -interface State { - toasts: ToasterToast[]; -} - -const toastTimeouts = new Map>(); - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return; - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId); - dispatch({ - type: 'REMOVE_TOAST', - toastId: toastId, - }); - }, TOAST_REMOVE_DELAY); - - toastTimeouts.set(toastId, timeout); -}; - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case 'ADD_TOAST': - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - }; - - case 'UPDATE_TOAST': - return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - }; - - case 'DISMISS_TOAST': { - const { toastId } = action; - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId); - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id); - }); - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - }; - } - case 'REMOVE_TOAST': - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - }; - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - }; - } -}; - -const listeners: Array<(state: State) => void> = []; - -let memoryState: State = { toasts: [] }; - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action); - listeners.forEach((listener) => { - listener(memoryState); - }); -} - -type Toast = Omit; - -function toast({ ...props }: Toast) { - const id = genId(); - - const update = (props: ToasterToast) => - dispatch({ - type: 'UPDATE_TOAST', - toast: { ...props, id }, - }); - const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id }); - - dispatch({ - type: 'ADD_TOAST', - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss(); - }, - }, - }); - - return { - id: id, - dismiss, - update, - }; -} - -function useToast() { - const [state, setState] = React.useState(memoryState); - - React.useEffect(() => { - listeners.push(setState); - return () => { - const index = listeners.indexOf(setState); - if (index > -1) { - listeners.splice(index, 1); - } - }; - }, [state]); - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }), - }; -} - -export { useToast, toast }; diff --git a/tsconfig.json b/tsconfig.json index 09d7454..925e5f8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,12 @@ "useDefineForClassFields": true, "forceConsistentCasingInFileNames": true }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "app/types/global.d.ts" // 显式包含全局声明文件 + ], "exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts", ".next"] }