forked from 77media/video-flow
全局消息配置
This commit is contained in:
parent
8590196ff8
commit
0d31522709
@ -1,4 +1,3 @@
|
|||||||
import { message } from "antd";
|
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,11 +51,7 @@ export const errorHandle = debounce(
|
|||||||
customMessage || HTTP_ERROR_MESSAGES[code] || DEFAULT_ERROR_MESSAGE;
|
customMessage || HTTP_ERROR_MESSAGES[code] || DEFAULT_ERROR_MESSAGE;
|
||||||
|
|
||||||
// 显示错误提示
|
// 显示错误提示
|
||||||
message.error({
|
window.msg.error(errorMessage);
|
||||||
content: errorMessage,
|
|
||||||
duration: 3,
|
|
||||||
className: 'custom-error-message'
|
|
||||||
});
|
|
||||||
|
|
||||||
// 执行特殊错误码的处理函数
|
// 执行特殊错误码的处理函数
|
||||||
const handler = ERROR_HANDLERS[code];
|
const handler = ERROR_HANDLERS[code];
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, AxiosHeaders } from 'axios';
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, AxiosHeaders } from 'axios';
|
||||||
import { message } from "antd";
|
|
||||||
import { BASE_URL } from './constants'
|
import { BASE_URL } from './constants'
|
||||||
import { errorHandle } from './errorHandle';
|
import { errorHandle } from './errorHandle';
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
|
|
||||||
import { TopBar } from "@/components/layout/top-bar";
|
import { TopBar } from "@/components/layout/top-bar";
|
||||||
import { HomePage2 } from "@/components/pages/home-page2";
|
import { HomePage2 } from "@/components/pages/home-page2";
|
||||||
import OAuthCallbackHandler from "@/components/ui/oauth-callback-handler";
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBar collapsed={true} />
|
<TopBar collapsed={true} />
|
||||||
<OAuthCallbackHandler />
|
|
||||||
<HomePage2 />
|
<HomePage2 />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { CreateMovieProjectV2Request, CreateMovieProjectV3Request } from "@/api/DTO/movie_start_dto";
|
|
||||||
import { createMovieProject, createMovieProjectV2, createMovieProjectV3 } from "@/api/create_movie";
|
import { createMovieProject, createMovieProjectV2, createMovieProjectV3 } from "@/api/create_movie";
|
||||||
import { QueueResponse, QueueStatus, withQueuePolling, QueueResponseData } from "@/api/movie_queue";
|
import { QueueResponse, withQueuePolling, QueueResponseData } from "@/api/movie_queue";
|
||||||
import { message } from "antd";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 电影项目创建模式
|
* 电影项目创建模式
|
||||||
@ -56,13 +55,13 @@ export class MovieProjectService {
|
|||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
if (error.message === '操作已取消') {
|
if (error.message === '操作已取消') {
|
||||||
message.info('Queue cancelled');
|
window.msg.info('Queue cancelled');
|
||||||
} else {
|
} 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: () => {
|
onCancel: () => {
|
||||||
message.info('Queue cancelled');
|
window.msg.info('Queue cancelled');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -80,7 +79,7 @@ export class MovieProjectService {
|
|||||||
if (error instanceof Error && error.message === '操作已取消') {
|
if (error instanceof Error && error.message === '操作已取消') {
|
||||||
throw error;
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { message } from "antd";
|
|
||||||
import { StoryTemplateEntity } from "../domain/Entities";
|
import { StoryTemplateEntity } from "../domain/Entities";
|
||||||
import { useUploadFile } from "../domain/service";
|
import { useUploadFile } from "../domain/service";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
|
|||||||
16
app/types/global.d.ts
vendored
Normal file
16
app/types/global.d.ts
vendored
Normal file
@ -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<typeof toast.promise>;
|
||||||
|
dismiss: () => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
92
components/common/GlobalMessage.tsx
Normal file
92
components/common/GlobalMessage.tsx
Normal file
@ -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<ToastT>) {
|
||||||
|
toast.success(message, {
|
||||||
|
icon: <CheckCircle2 className="h-4 w-4" />,
|
||||||
|
duration,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string, duration = 3000, options?: Partial<ToastT>) {
|
||||||
|
toast.error(message, {
|
||||||
|
icon: <XCircle className="h-4 w-4" />,
|
||||||
|
duration,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
warning(message: string, duration = 3000, options?: Partial<ToastT>) {
|
||||||
|
toast.warning(message, {
|
||||||
|
icon: <AlertCircle className="h-4 w-4" />,
|
||||||
|
duration,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message: string, duration = 3000, options?: Partial<ToastT>) {
|
||||||
|
toast(message, {
|
||||||
|
icon: <Info className="h-4 w-4" />,
|
||||||
|
duration,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loading(message: string, options?: Partial<ToastT>) {
|
||||||
|
return toast.promise(
|
||||||
|
new Promise(() => {}),
|
||||||
|
{
|
||||||
|
loading: message,
|
||||||
|
icon: <Loader2 className="h-4 w-4 animate-spin" />,
|
||||||
|
...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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -239,9 +239,9 @@ export function TopBar({ collapsed }: { collapsed: boolean }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Notifications */}
|
{/* Notifications */}
|
||||||
{/* <Button variant="ghost" size="sm" onClick={() => showQueueNotification(3, 10)}>
|
<Button variant="ghost" size="sm" onClick={() => window.msg.error('Loading...')}>
|
||||||
<Bell className="h-4 w-4" />
|
<Bell className="h-4 w-4" />
|
||||||
</Button> */}
|
</Button>
|
||||||
|
|
||||||
{/* Theme Toggle */}
|
{/* Theme Toggle */}
|
||||||
{/* <Button
|
{/* <Button
|
||||||
|
|||||||
@ -287,14 +287,21 @@ export default function CreateToVideo2() {
|
|||||||
{/* 加载更多指示器 */}
|
{/* 加载更多指示器 */}
|
||||||
{isLoadingMore && (
|
{isLoadingMore && (
|
||||||
<div className="flex justify-center py-12">
|
<div className="flex justify-center py-12">
|
||||||
<div className="flex items-center gap-3 px-6 py-3 bg-black/30 backdrop-blur-xl border border-white/10 rounded-full">
|
<div className="flex items-center">
|
||||||
<Loader2 className="w-5 h-5 animate-spin text-purple-400" />
|
<Loader2 className="w-5 h-5 animate-spin text-purple-400" />
|
||||||
<span className="text-white/90 font-medium">Loading more projects...</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{episodeList.length === 0 && isLoading && (
|
||||||
|
<div className="flex justify-center py-12 h-full">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-purple-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 视频工具组件 - 使用独立组件 */}
|
{/* 视频工具组件 - 使用独立组件 */}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovi
|
|||||||
import { useScriptService } from "@/app/service/Interaction/ScriptService";
|
import { useScriptService } from "@/app/service/Interaction/ScriptService";
|
||||||
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
|
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
|
||||||
import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
|
import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
|
||||||
import { msg } from '@/utils/message';
|
|
||||||
|
|
||||||
interface UseWorkflowDataProps {
|
interface UseWorkflowDataProps {
|
||||||
onEditPlanGenerated?: () => void;
|
onEditPlanGenerated?: () => void;
|
||||||
@ -209,7 +208,7 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps =
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isShowError) {
|
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]);
|
}, [isShowError]);
|
||||||
|
|
||||||
|
|||||||
@ -3,15 +3,11 @@
|
|||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { store } from '@/lib/store/store';
|
import { store } from '@/lib/store/store';
|
||||||
import { ThemeProvider } from 'next-themes';
|
import { ThemeProvider } from 'next-themes';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from 'sonner';
|
||||||
import AuthGuard from './auth/auth-guard';
|
import AuthGuard from './auth/auth-guard';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
import { registerGlobalMessage } from '@/components/common/GlobalMessage';
|
||||||
// 动态导入 OAuthCallbackHandler 和 DevHelper
|
import { useEffect } from 'react';
|
||||||
const OAuthCallbackHandler = dynamic(
|
|
||||||
() => import('./ui/oauth-callback-handler').then(mod => mod.default),
|
|
||||||
{ ssr: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
const DevHelper = dynamic(
|
const DevHelper = dynamic(
|
||||||
() => import('@/utils/dev-helper').then(mod => (mod as any).default),
|
() => import('@/utils/dev-helper').then(mod => (mod as any).default),
|
||||||
@ -19,6 +15,11 @@ const DevHelper = dynamic(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export function Providers({ children }: { children: React.ReactNode }) {
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
|
// 注册全局消息提醒
|
||||||
|
useEffect(() => {
|
||||||
|
registerGlobalMessage();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
@ -30,8 +31,23 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
{children}
|
{children}
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
<Toaster />
|
<Toaster
|
||||||
<OAuthCallbackHandler />
|
position="bottom-left"
|
||||||
|
theme="dark"
|
||||||
|
richColors
|
||||||
|
toastOptions={{
|
||||||
|
// 统一配置所有 toast 的样式
|
||||||
|
classNames: {
|
||||||
|
toast: "dark:bg-zinc-900 dark:text-zinc-100 dark:border-zinc-800",
|
||||||
|
title: "dark:text-zinc-100 font-medium",
|
||||||
|
description: "dark:text-zinc-400",
|
||||||
|
actionButton: "dark:bg-zinc-700 dark:text-zinc-100",
|
||||||
|
cancelButton: "dark:bg-zinc-600 dark:text-zinc-100",
|
||||||
|
closeButton: "dark:text-zinc-300 hover:text-zinc-100"
|
||||||
|
},
|
||||||
|
duration: 3000,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{process.env.NODE_ENV === 'development' && <DevHelper />}
|
{process.env.NODE_ENV === 'development' && <DevHelper />}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { ScriptData, ScriptBlock, ScriptContent, ThemeTagBgColor, ThemeType } fr
|
|||||||
import ContentEditable, { ContentEditableEvent } from 'react-contenteditable';
|
import ContentEditable, { ContentEditableEvent } from 'react-contenteditable';
|
||||||
import { SelectDropdown } from '@/components/ui/select-dropdown';
|
import { SelectDropdown } from '@/components/ui/select-dropdown';
|
||||||
import { TypewriterText } from '@/components/workflow/work-office/common/TypewriterText';
|
import { TypewriterText } from '@/components/workflow/work-office/common/TypewriterText';
|
||||||
import { msg } from '@/utils/message';
|
|
||||||
|
|
||||||
interface ScriptRendererProps {
|
interface ScriptRendererProps {
|
||||||
data: any[];
|
data: any[];
|
||||||
@ -126,7 +125,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
|
|||||||
const handleThemeTagChange = (value: string[]) => {
|
const handleThemeTagChange = (value: string[]) => {
|
||||||
console.log('主题标签更改', value);
|
console.log('主题标签更改', value);
|
||||||
if (value.length > 5) {
|
if (value.length > 5) {
|
||||||
msg.error('最多可选择5个主题标签', 3000);
|
window.msg.error('max 5 theme tags', 3000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAddThemeTag(value);
|
setAddThemeTag(value);
|
||||||
@ -217,7 +216,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
|
|||||||
className="w-6 h-6 p-1 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors"
|
className="w-6 h-6 p-1 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// 提示权限不够
|
// 提示权限不够
|
||||||
msg.error('No permission!');
|
window.msg.error('No permission!');
|
||||||
return;
|
return;
|
||||||
handleEditBlock(block);
|
handleEditBlock(block);
|
||||||
}}
|
}}
|
||||||
@ -226,7 +225,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
|
|||||||
className="w-6 h-6 p-1 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors"
|
className="w-6 h-6 p-1 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(block.content.map(item => item.text).join('\n'));
|
navigator.clipboard.writeText(block.content.map(item => item.text).join('\n'));
|
||||||
msg.success('Copied!');
|
window.msg.success('Copied!');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import { useEditData } from '@/components/pages/work-flow/use-edit-data';
|
|||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { RoleEntity } from '@/app/service/domain/Entities';
|
import { RoleEntity } from '@/app/service/domain/Entities';
|
||||||
import { Role } from '@/api/DTO/movieEdit';
|
import { Role } from '@/api/DTO/movieEdit';
|
||||||
import { msg } from '@/utils/message';
|
|
||||||
|
|
||||||
interface CharacterTabContentProps {
|
interface CharacterTabContentProps {
|
||||||
originalRoles: Role[];
|
originalRoles: Role[];
|
||||||
@ -113,7 +112,7 @@ CharacterTabContentProps
|
|||||||
|
|
||||||
const handleSmartPolish = (text: string) => {
|
const handleSmartPolish = (text: string) => {
|
||||||
// 然后调用优化角色文本
|
// 然后调用优化角色文本
|
||||||
msg.error('No permission!');
|
window.msg.error('No permission!');
|
||||||
return;
|
return;
|
||||||
optimizeRoleText(text);
|
optimizeRoleText(text);
|
||||||
};
|
};
|
||||||
@ -212,7 +211,7 @@ CharacterTabContentProps
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenReplaceLibrary = async () => {
|
const handleOpenReplaceLibrary = async () => {
|
||||||
msg.error('No permission!');
|
window.msg.error('No permission!');
|
||||||
return;
|
return;
|
||||||
setIsLoadingLibrary(true);
|
setIsLoadingLibrary(true);
|
||||||
setIsReplaceLibraryOpen(true);
|
setIsReplaceLibraryOpen(true);
|
||||||
@ -222,7 +221,7 @@ CharacterTabContentProps
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRegenerate = async () => {
|
const handleRegenerate = async () => {
|
||||||
msg.error('No permission!');
|
window.msg.error('No permission!');
|
||||||
return;
|
return;
|
||||||
console.log('Regenerate');
|
console.log('Regenerate');
|
||||||
setIsRegenerate(true);
|
setIsRegenerate(true);
|
||||||
@ -237,7 +236,7 @@ CharacterTabContentProps
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUploadClick = () => {
|
const handleUploadClick = () => {
|
||||||
msg.error('No permission!');
|
window.msg.error('No permission!');
|
||||||
return;
|
return;
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import { MusicTabContent } from './music-tab-content';
|
|||||||
import FloatingGlassPanel from './FloatingGlassPanel';
|
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||||
import { SaveEditUseCase } from '@/app/service/usecase/SaveEditUseCase';
|
import { SaveEditUseCase } from '@/app/service/usecase/SaveEditUseCase';
|
||||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||||
import { msg } from '@/utils/message';
|
|
||||||
|
|
||||||
interface EditModalProps {
|
interface EditModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -124,7 +123,7 @@ export function EditModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
msg.error('No permission!');
|
window.msg.error('No permission!');
|
||||||
return;
|
return;
|
||||||
console.log('handleSave');
|
console.log('handleSave');
|
||||||
// setIsRemindFallbackOpen(true);
|
// setIsRemindFallbackOpen(true);
|
||||||
@ -143,7 +142,7 @@ export function EditModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirmGotoFallback = () => {
|
const handleConfirmGotoFallback = () => {
|
||||||
msg.error('No permission!');
|
window.msg.error('No permission!');
|
||||||
return;
|
return;
|
||||||
setDisabledBtn(true);
|
setDisabledBtn(true);
|
||||||
console.log('handleConfirmGotoFallback');
|
console.log('handleConfirmGotoFallback');
|
||||||
@ -170,7 +169,7 @@ export function EditModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
msg.error('No permission!');
|
window.msg.error('No permission!');
|
||||||
return;
|
return;
|
||||||
console.log('handleReset');
|
console.log('handleReset');
|
||||||
// 重置当前tab修改的数据
|
// 重置当前tab修改的数据
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -2,7 +2,6 @@ import React, { forwardRef, useRef, useState } from "react";
|
|||||||
import { useDeepCompareEffect } from "@/hooks/useDeepCompareEffect";
|
import { useDeepCompareEffect } from "@/hooks/useDeepCompareEffect";
|
||||||
import { Plus, X, UserRoundPlus, MessageCirclePlus, MessageCircleMore, ClipboardType } from "lucide-react";
|
import { Plus, X, UserRoundPlus, MessageCirclePlus, MessageCircleMore, ClipboardType } from "lucide-react";
|
||||||
import ShotEditor from "./ShotEditor";
|
import ShotEditor from "./ShotEditor";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { TextToShotAdapter } from "@/app/service/adapter/textToShot";
|
import { TextToShotAdapter } from "@/app/service/adapter/textToShot";
|
||||||
|
|
||||||
|
|
||||||
@ -90,11 +89,7 @@ export const ShotsEditor = forwardRef<any, ShotsEditorProps>(function ShotsEdito
|
|||||||
|
|
||||||
const addShot = () => {
|
const addShot = () => {
|
||||||
if (shots.length > 3) {
|
if (shots.length > 3) {
|
||||||
toast.error('No more than 4 shots', {
|
window.msg.error('max 4 shots');
|
||||||
duration: 3000,
|
|
||||||
position: 'top-center',
|
|
||||||
richColors: true,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newShot = createEmptyShot();
|
const newShot = createEmptyShot();
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import HorizontalScroller from './HorizontalScroller';
|
|||||||
import { useEditData } from '@/components/pages/work-flow/use-edit-data';
|
import { useEditData } from '@/components/pages/work-flow/use-edit-data';
|
||||||
import { RoleEntity, VideoSegmentEntity } from '@/app/service/domain/Entities';
|
import { RoleEntity, VideoSegmentEntity } from '@/app/service/domain/Entities';
|
||||||
import { ShotVideo } from '@/api/DTO/movieEdit';
|
import { ShotVideo } from '@/api/DTO/movieEdit';
|
||||||
import { msg } from '@/utils/message';
|
|
||||||
|
|
||||||
interface ShotTabContentProps {
|
interface ShotTabContentProps {
|
||||||
currentSketchIndex: number;
|
currentSketchIndex: number;
|
||||||
@ -448,7 +447,7 @@ export const ShotTabContent = forwardRef<
|
|||||||
{/* 人物替换按钮 */}
|
{/* 人物替换按钮 */}
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
msg.error('No permission!');
|
window.msg.error('No permission!');
|
||||||
return;
|
return;
|
||||||
handleScan()
|
handleScan()
|
||||||
}}
|
}}
|
||||||
@ -504,7 +503,7 @@ export const ShotTabContent = forwardRef<
|
|||||||
</motion.button> */}
|
</motion.button> */}
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
msg.error('No permission!');
|
window.msg.error('No permission!');
|
||||||
return;
|
return;
|
||||||
handleRegenerate();
|
handleRegenerate();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useTheme } from 'next-themes';
|
|
||||||
import { Toaster as Sonner } from 'sonner';
|
|
||||||
|
|
||||||
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
|
||||||
const { theme = 'system' } = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Sonner
|
|
||||||
theme={theme as ToasterProps['theme']}
|
|
||||||
className="toaster group"
|
|
||||||
toastOptions={{
|
|
||||||
classNames: {
|
|
||||||
toast:
|
|
||||||
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
|
|
||||||
description: 'group-[.toast]:text-muted-foreground',
|
|
||||||
actionButton:
|
|
||||||
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
|
|
||||||
cancelButton:
|
|
||||||
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Toaster };
|
|
||||||
@ -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<typeof ToastPrimitives.Viewport>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Viewport
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
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<typeof ToastPrimitives.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
|
||||||
VariantProps<typeof toastVariants>
|
|
||||||
>(({ className, variant, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<ToastPrimitives.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(toastVariants({ variant }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
|
||||||
|
|
||||||
const ToastAction = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Action
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
|
||||||
|
|
||||||
const ToastClose = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Close
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
toast-close=""
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</ToastPrimitives.Close>
|
|
||||||
));
|
|
||||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
|
||||||
|
|
||||||
const ToastTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn('text-sm font-semibold', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
|
||||||
|
|
||||||
const ToastDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ToastPrimitives.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn('text-sm opacity-90', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
|
||||||
|
|
||||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
|
||||||
|
|
||||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
|
||||||
|
|
||||||
export {
|
|
||||||
type ToastProps,
|
|
||||||
type ToastActionElement,
|
|
||||||
ToastProvider,
|
|
||||||
ToastViewport,
|
|
||||||
Toast,
|
|
||||||
ToastTitle,
|
|
||||||
ToastDescription,
|
|
||||||
ToastClose,
|
|
||||||
ToastAction,
|
|
||||||
};
|
|
||||||
@ -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 (
|
|
||||||
<ToastProvider>
|
|
||||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
|
||||||
return (
|
|
||||||
<Toast key={id} {...props}>
|
|
||||||
<div className="grid gap-1">
|
|
||||||
{title && <ToastTitle>{title}</ToastTitle>}
|
|
||||||
{description && (
|
|
||||||
<ToastDescription>{description}</ToastDescription>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{action}
|
|
||||||
<ToastClose />
|
|
||||||
</Toast>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<ToastViewport />
|
|
||||||
</ToastProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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<ToasterToast>;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: ActionType['DISMISS_TOAST'];
|
|
||||||
toastId?: ToasterToast['id'];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: ActionType['REMOVE_TOAST'];
|
|
||||||
toastId?: ToasterToast['id'];
|
|
||||||
};
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
toasts: ToasterToast[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
||||||
|
|
||||||
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<ToasterToast, 'id'>;
|
|
||||||
|
|
||||||
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<State>(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 };
|
|
||||||
@ -25,6 +25,12 @@
|
|||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"forceConsistentCasingInFileNames": 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"]
|
"exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts", ".next"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user