diff --git a/.env.development b/.env.development index 8d5e823..2878ea4 100644 --- a/.env.development +++ b/.env.development @@ -1,8 +1,9 @@ -# NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com -# NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com -NEXT_PUBLIC_JAVA_URL = http://test.java.movieflow.ai -NEXT_PUBLIC_BASE_URL = http://test.video.movieflow.ai +NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com +NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com +NEXT_PUBLIC_CUT_URL = https://smartcut.movieflow.ai +# NEXT_PUBLIC_JAVA_URL = https://test.java.movieflow.ai +# NEXT_PUBLIC_BASE_URL = https://test.video.movieflow.ai # 失败率 -NEXT_PUBLIC_ERROR_CONFIG = 0.2 \ No newline at end of file +NEXT_PUBLIC_ERROR_CONFIG = 0.5 \ No newline at end of file diff --git a/.env.production b/.env.production index 8816191..3bb1344 100644 --- a/.env.production +++ b/.env.production @@ -1,7 +1,9 @@ -# NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com -# NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com -NEXT_PUBLIC_JAVA_URL = http://test.java.movieflow.ai -NEXT_PUBLIC_BASE_URL = http://test.video.movieflow.ai + +#NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com +#NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com +NEXT_PUBLIC_JAVA_URL = https://auth.movieflow.ai +NEXT_PUBLIC_BASE_URL = https://api.video.movieflow.ai +NEXT_PUBLIC_CUT_URL = https://smartcut.movieflow.ai # 失败率 NEXT_PUBLIC_ERROR_CONFIG = 0.2 diff --git a/api/DTO/movieEdit.ts b/api/DTO/movieEdit.ts index 28745c0..45690ec 100644 --- a/api/DTO/movieEdit.ts +++ b/api/DTO/movieEdit.ts @@ -363,6 +363,10 @@ export interface VideoFlowProjectResponse { last_message: string; /** 项目内容 */ data: ProjectContentData; + /** 最终简单视频 */ + final_simple_video: string; + /** 最终视频 */ + final_video: string; } /** * 新角色列表项接口 diff --git a/api/errorHandle.ts b/api/errorHandle.ts index 50b7fa7..67172c8 100644 --- a/api/errorHandle.ts +++ b/api/errorHandle.ts @@ -1,4 +1,3 @@ -import { message } from "antd"; import { debounce } from "lodash"; /** @@ -8,6 +7,7 @@ const HTTP_ERROR_MESSAGES: Record = { 0: "Please try again if the network is abnormal. If it happens again, please contact us.", 400: "Request parameter error, please check your input.", 401: "Login expired, please log in again.", + 402: "Insufficient points, please recharge.", 403: "Insufficient permissions to access this resource.", 404: "Requested resource does not exist.", 408: "Request timeout, please try again.", @@ -32,12 +32,6 @@ const ERROR_HANDLERS: Record void> = { localStorage.removeItem('token'); // 跳转到登录页面 window.location.href = '/login'; - }, - 4001: () => { - // 显示积分不足通知 - import('../utils/notifications').then(({ showInsufficientPointsNotification }) => { - showInsufficientPointsNotification(); - }); } }; @@ -52,11 +46,10 @@ export const errorHandle = debounce( customMessage || HTTP_ERROR_MESSAGES[code] || DEFAULT_ERROR_MESSAGE; // 显示错误提示 - message.error({ - content: errorMessage, - duration: 3, - className: 'custom-error-message' - }); + // 402 状态码 不需要显示提示 + if (code !== 402) { + window.msg.error(errorMessage); + } // 执行特殊错误码的处理函数 const handler = ERROR_HANDLERS[code]; diff --git a/api/request.ts b/api/request.ts index 7a59dbf..4bc2b41 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'; @@ -11,15 +10,24 @@ import { errorHandle } from './errorHandle'; const handleRequestError = (error: any, defaultMessage: string = '请求失败') => { if (error.response) { const { status, data } = error.response; - const errorMessage = data?.message || defaultMessage; + const errorMessage = data?.message || data?.detail?.message || defaultMessage; + + // 处理 402 状态码的特殊情况 + if (status === 402 && data?.detail) { + // 只显示通知,不调用 errorHandle + import('../utils/notifications').then(({ showInsufficientPointsNotification }) => { + showInsufficientPointsNotification(data.detail); + }); + return; // 直接返回,不再抛出错误 + } + errorHandle(status, errorMessage); } else if (error.request) { - // 请求已发出但没有收到响应 - errorHandle(0 ); + errorHandle(0); } else { - // 请求配置出错 errorHandle(0, error.message || defaultMessage); } + return Promise.reject(error); // 将 reject 移到这里,避免 402 时重复处理 }; // 创建 axios 实例 const request: AxiosInstance = axios.create({ diff --git a/api/video_flow.ts b/api/video_flow.ts index e88b38d..a4f2cec 100644 --- a/api/video_flow.ts +++ b/api/video_flow.ts @@ -292,8 +292,8 @@ export const getRunningStreamData = async (data: { }; // 获取 生成剪辑计划 接口 -export const getGenerateEditPlan = async (data: GenerateEditPlanRequest): Promise> => { - return post>("/edit-plan/generate-by-project", data); +export const getGenerateEditPlan = async (data: GenerateEditPlanRequest): Promise> => { + return post>("/edit-plan/generate-by-project", data); }; /** 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..c946d4b 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"; @@ -69,6 +68,15 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { setIsLoading(true); const templates = await templateStoryUseCase.getTemplateStoryList(); + templates.forEach(template => { + if (template.template_id === '69') { + template.freeInputItem = { + user_tips: "How is coffee made?", + constraints: "", + free_input_text: "" + }; + } + }); setTemplateStoryList(templates); setSelectedTemplate(templates[0]); @@ -238,7 +246,7 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { setIsLoading(true); const params: CreateMovieProjectV3Request = { - script: selectedTemplate?.generateText || "", + script: selectedTemplate?.freeInputItem?.free_input_text || selectedTemplate?.generateText || "", category: selectedTemplate?.category || "", user_id, mode, diff --git a/app/service/domain/Entities.ts b/app/service/domain/Entities.ts index da780fd..0f26ff8 100644 --- a/app/service/domain/Entities.ts +++ b/app/service/domain/Entities.ts @@ -172,4 +172,13 @@ export interface StoryTemplateEntity { /** 道具照片URL */ photo_url: string; }[]; + /** 自由输入文字 */ + freeInputItem?: { + /** 用户提示,提示给用户需要输入什么内容 */ + user_tips: string; + /** 约束,可选,用于传给ai,让ai去拦截用户不符合约束的输入内容 */ + constraints: string; + /** 自由输入文字 */ + free_input_text: string; + } } 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/compile_mf.sh b/compile_mf.sh deleted file mode 100755 index a86f7fc..0000000 --- a/compile_mf.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash - -BRANCH_NAME="dev" - -# 修改项目名称 -PROJECT_NAME="video-flow-frontend" - -# 设置日志文件路径 -LOGFILE="build_and_copy.log" - -# 记录开始时间 -echo "Build process started at $(date)" | tee $LOGFILE - -# 获取当前分支名 -current_branch=$(git rev-parse --abbrev-ref HEAD) - -# 打包之前,需要检查是否在 dev 分支,工作区是否干净,是否和远程分支一致 -if [ "$(git branch --show-current)" != "$BRANCH_NAME" ]; then - echo "当前分支不是 dev 分支" - exit 1 -fi - -# 检查工作区是否干净 -if [ -n "$(git status --porcelain)" ]; then - echo "工作区不干净" - exit 1 -fi - -# 检查远程分支是否和本地分支一致 -if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/$BRANCH_NAME)" ]; then - echo "本地分支和远程分支不一致" - exit 1 -fi - - -# 检查当前分支并运行相应的 npm 命令 -if [ "$current_branch" = "$BRANCH_NAME" ]; then - echo "On dev branch, building project..." | tee -a $LOGFILE - PROFILE_ENV=$BRANCH_NAME - - # 安装依赖并构建 - yarn install - yarn build - - # 准备dist目录 - mkdir -p dist - cp -r .next dist/ - cp -r public dist/ - cp package.json dist/ - cp package-lock.json dist/ - -else - echo "On non-dev branch ($current_branch), exiting" - exit 1 -fi - -# 创建tar包 -tar -czvf $PROJECT_NAME-$PROFILE_ENV.tar.gz dist - -# 记录结束时间 -echo "Build process completed at $(date)" | tee -a $LOGFILE - -# 上传到 nexus -echo "upload to nexus at $(date)" | tee -a $LOGFILE -curl -u 'admin':'YZ9Gq6=8\*G|?:,' --upload-file $PROJECT_NAME-$PROFILE_ENV.tar.gz https://repo.qikongjian.com/repository/frontend-tar-files/ - -# 清理构建文件 -rm -rf dist -rm $PROJECT_NAME-$PROFILE_ENV.tar.gz diff --git a/compile_pre.sh b/compile_pre.sh deleted file mode 100755 index 22b2adc..0000000 --- a/compile_pre.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash - -BRANCH_NAME="dev" - -# 修改项目名称 -PROJECT_NAME="video-flow-$BRANCH_NAME-frontend" - -# 设置日志文件路径 -LOGFILE="build_and_copy.log" - -# 记录开始时间 -echo "Build process started at $(date)" | tee $LOGFILE - -# 获取当前分支名 -current_branch=$(git rev-parse --abbrev-ref HEAD) - -# 打包之前,需要检查是否在 dev 分支,工作区是否干净,是否和远程分支一致 -if [ "$(git branch --show-current)" != "$BRANCH_NAME" ]; then - echo "当前分支不是 dev 分支" - exit 1 -fi - -# 检查工作区是否干净 -if [ -n "$(git status --porcelain)" ]; then - echo "工作区不干净" - exit 1 -fi - -# 检查远程分支是否和本地分支一致 -if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/$BRANCH_NAME)" ]; then - echo "本地分支和远程分支不一致" - exit 1 -fi - - -# 检查当前分支并运行相应的 npm 命令 -if [ "$current_branch" = "$BRANCH_NAME" ]; then - echo "On dev branch, building project..." | tee -a $LOGFILE - PROFILE_ENV=$BRANCH_NAME - - # 安装依赖并构建 - yarn install - yarn build - - # 准备dist目录 - mkdir -p dist - cp -r .next dist/ - cp -r public dist/ - cp package.json dist/ - cp package-lock.json dist/ - -else - echo "On non-dev branch ($current_branch), exiting" - exit 1 -fi - -# 创建tar包 -tar -czvf $PROJECT_NAME-$PROFILE_ENV.tar.gz dist - -# 记录结束时间 -echo "Build process completed at $(date)" | tee -a $LOGFILE - -# 上传到 nexus -echo "upload to nexus at $(date)" | tee -a $LOGFILE -curl -u 'admin':'YZ9Gq6=8\*G|?:,' --upload-file $PROJECT_NAME-$PROFILE_ENV.tar.gz https://repo.qikongjian.com/repository/frontend-tar-files/ - -# 清理构建文件 -rm -rf dist -rm $PROJECT_NAME-$PROFILE_ENV.tar.gz diff --git a/components/ChatInputBox/ChatInputBox.tsx b/components/ChatInputBox/ChatInputBox.tsx index c32b6e0..7d18121 100644 --- a/components/ChatInputBox/ChatInputBox.tsx +++ b/components/ChatInputBox/ChatInputBox.tsx @@ -46,6 +46,24 @@ import { HighlightEditor } from "../common/HighlightEditor"; import GlobalLoad from "../common/GlobalLoad"; /**模板故事模式弹窗组件 */ +/** + * 防抖函数 + * @param {Function} func - 需要防抖的函数 + * @param {number} wait - 等待时间(ms) + * @returns {Function} - 防抖后的函数 + */ +const debounce = (func: Function, wait: number) => { + let timeout: NodeJS.Timeout; + return function executedFunction(...args: any[]) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +}; + const RenderTemplateStoryMode = ({ isTemplateCreating, setIsTemplateCreating, @@ -93,6 +111,22 @@ const RenderTemplateStoryMode = ({ clearData, } = useTemplateStoryServiceHook(); + // 防抖处理的输入更新函数 + const debouncedUpdateInput = debounce((value: string) => { + // 过滤特殊字符 + const sanitizedValue = value.replace(/[<>]/g, ''); + // 更新输入值 + if (!selectedTemplate?.freeInputItem) return; + const updatedTemplate: StoryTemplateEntity = { + ...selectedTemplate, + freeInputItem: { + ...selectedTemplate.freeInputItem, + free_input_text: sanitizedValue + } + }; + setSelectedTemplate(updatedTemplate); + }, 300); // 300ms 的防抖延迟 + // 使用上传文件hook const { uploadFile, isUploading } = useUploadFile(); // 本地加载状态,用于 UI 反馈 @@ -258,6 +292,7 @@ const RenderTemplateStoryMode = ({ + {/* 角色配置区域 */} {selectedTemplate?.storyRole && selectedTemplate.storyRole.length > 0 && ( @@ -755,7 +790,29 @@ const RenderTemplateStoryMode = ({ */} -
+
+ {/** 自由输入文字 */} + {(selectedTemplate?.freeInputItem) && ( +
+ { + // 更新自由输入文字字段 + const updatedTemplate = { + ...selectedTemplate!, + freeInputItem: { + ...selectedTemplate!.freeInputItem, + free_input_text: e.target.value + } + }; + setSelectedTemplate(updatedTemplate as StoryTemplateEntity); + }} + /> +
+ )} 0} handleCreateVideo={handleConfirm} diff --git a/components/auth/auth-guard.tsx b/components/auth/auth-guard.tsx index 031197c..2699e6d 100644 --- a/components/auth/auth-guard.tsx +++ b/components/auth/auth-guard.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useRouter, usePathname } from 'next/navigation'; -import { checkAuth, clearAuthData, getUserProfile, isAuthenticated } from '@/lib/auth'; +import { clearAuthData, getUserProfile, isAuthenticated } from '@/lib/auth'; import GlobalLoad from '../common/GlobalLoad'; import { message } from 'antd'; import { errorHandle } from '@/api/errorHandle'; 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/dashboard-layout.tsx b/components/layout/dashboard-layout.tsx index 5e1ad14..0b7e5b9 100644 --- a/components/layout/dashboard-layout.tsx +++ b/components/layout/dashboard-layout.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import { Sidebar } from './sidebar'; import { TopBar } from './top-bar'; +import { useDeviceType } from '@/hooks/useDeviceType'; interface DashboardLayoutProps { children: React.ReactNode; @@ -10,15 +11,38 @@ interface DashboardLayoutProps { export function DashboardLayout({ children }: DashboardLayoutProps) { const [sidebarCollapsed, setSidebarCollapsed] = useState(true); // 默认收起状态 + const { deviceType, isMobile, isTablet, isDesktop } = useDeviceType(); + + // 根据设备类型设置布局样式 + const getLayoutStyles = () => { + if (isMobile) { + return { + left: '0', + width: '100vw' + }; + } + + if (isTablet) { + return { + left: sidebarCollapsed ? '3rem' : '12rem', + width: sidebarCollapsed ? 'calc(100vw - 3rem)' : 'calc(100vw - 12rem)' + }; + } + + // 桌面端 + return { + left: sidebarCollapsed ? '4rem' : '16rem', + width: sidebarCollapsed ? 'calc(100vw - 4rem)' : 'calc(100vw - 16rem)' + }; + }; return (
- - -
+ + {isDesktop && } +
{children}
diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx index 1f78b40..2516a30 100644 --- a/components/layout/top-bar.tsx +++ b/components/layout/top-bar.tsx @@ -25,6 +25,7 @@ import { getUserSubscriptionInfo, } from "@/lib/stripe"; import UserCard from "@/components/common/userCard"; +import { showInsufficientPointsNotification } from "@/utils/notifications"; interface User { id: string; @@ -35,7 +36,7 @@ interface User { plan_name?: string; } -export function TopBar({ collapsed }: { collapsed: boolean }) { +export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDesktop?: boolean }) { const router = useRouter(); const [isOpen, setIsOpen] = React.useState(false); const menuRef = useRef(null); @@ -169,10 +170,10 @@ export function TopBar({ collapsed }: { collapsed: boolean }) { className="fixed right-0 top-0 h-16 header z-[999]" style={{ isolation: "isolate", - left: pathname === "/" ? "0" : (collapsed ? "2.5rem" : "16rem") + left: (pathname === "/" || !isDesktop) ? "0" : (collapsed ? "2.5rem" : "16rem") }} > -
+
showQueueNotification(3, 10)}> + {/* */} @@ -273,7 +278,9 @@ export function TopBar({ collapsed }: { collapsed: boolean }) { padding: "unset !important", }} > -
+
{currentUser.username ? currentUser.username.charAt(0) : "MF"}
diff --git a/components/pages/create-to-video2.tsx b/components/pages/create-to-video2.tsx index 094c650..41fc667 100644 --- a/components/pages/create-to-video2.tsx +++ b/components/pages/create-to-video2.tsx @@ -287,14 +287,21 @@ export default function CreateToVideo2() { {/* 加载更多指示器 */} {isLoadingMore && (
-
+
- Loading more projects...
)}
)} + + {episodeList.length === 0 && isLoading && ( +
+
+ +
+
+ )}
{/* 视频工具组件 - 使用独立组件 */} diff --git a/components/pages/work-flow.tsx b/components/pages/work-flow.tsx index edd8e2f..0de4dad 100644 --- a/components/pages/work-flow.tsx +++ b/components/pages/work-flow.tsx @@ -60,7 +60,7 @@ const WorkFlow = React.memo(function WorkFlow() { showEditingNotification({ description: 'Performing intelligent editing...', successDescription: 'Editing successful', - timeoutDescription: 'Editing failed, please try again', + timeoutDescription: 'Editing failed. Please click the edit button to go to the smart editing platform.', timeout: 5 * 60 * 1000, key: editingNotificationKey.current, onFail: () => { diff --git a/components/pages/work-flow/ai-editing-iframe.tsx b/components/pages/work-flow/ai-editing-iframe.tsx index 597575a..8b18bfb 100644 --- a/components/pages/work-flow/ai-editing-iframe.tsx +++ b/components/pages/work-flow/ai-editing-iframe.tsx @@ -92,9 +92,11 @@ export const AIEditingIframe = React.forwardRef(null); const progressIntervalRef = useRef(null); const timeoutRef = useRef(null); + const cutUrl = process.env.NEXT_PUBLIC_CUT_URL || 'https://cut.movieflow.ai'; + console.log('cutUrl', cutUrl); // 构建智能剪辑URL - const aiEditingUrl = `https://smartcut.movieflow.ai/ai-editor/${projectId}?token=${token}&user_id=${userId}&auto=true&embedded=true`; + const aiEditingUrl = `${cutUrl}/ai-editor/${projectId}?token=${token}&user_id=${userId}&auto=true&embedded=true`; /** * 监听iframe消息 @@ -102,7 +104,7 @@ export const AIEditingIframe = React.forwardRef { const handleMessage = (event: MessageEvent) => { // 验证消息来源 - if (!event.origin.includes('smartcut.movieflow.ai')) { + if (!event.origin.includes(cutUrl)) { return; } diff --git a/components/pages/work-flow/media-viewer.tsx b/components/pages/work-flow/media-viewer.tsx index 419f9ed..9f0f90f 100644 --- a/components/pages/work-flow/media-viewer.tsx +++ b/components/pages/work-flow/media-viewer.tsx @@ -427,6 +427,7 @@ export const MediaViewer = React.memo(function MediaViewer({ // 渲染视频内容 const renderVideoContent = (onGotoCut: () => void) => { + if (!taskObject.videos.data[currentSketchIndex]) return null; const urls = taskObject.videos.data[currentSketchIndex]?.urls ? taskObject.videos.data[currentSketchIndex]?.urls.join(',') : ''; return (
void; @@ -22,6 +21,9 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = const useid = JSON.parse(localStorage.getItem("currentUser") || '{}').id || NaN; const notificationKey = useMemo(() => `video-workflow-${episodeId}`, [episodeId]); + const cutUrl = process.env.NEXT_PUBLIC_CUT_URL || 'https://cut.movieflow.ai'; + console.log('cutUrl', cutUrl); + useEffect(() => { console.log("init-useWorkflowData"); return () => { @@ -74,6 +76,8 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = const [isShowError, setIsShowError] = useState(false); const [isAnalyzing, setIsAnalyzing] = useState(false); const [isGenerateEditPlan, setIsGenerateEditPlan] = useState(false); + const [retryCount, setRetryCount] = useState(0); + const [isLoadingGenerateEditPlan, setIsLoadingGenerateEditPlan] = useState(false); const [state, setState] = useState({ mode: 'automatic' as 'automatic' | 'manual' | 'auto', originalText: '', @@ -135,7 +139,7 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = } }, [taskObject.currentStage]); - const generateEditPlan = useCallback(async () => { + const generateEditPlan = useCallback(async (retryCount: number) => { if (isLoadedRef.current) { return; } @@ -147,9 +151,9 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = // 更新通知内容 showEditingNotification({ key: notificationKey, - description: 'Generating intelligent editing plan...', - successDescription: '剪辑计划生成完成', - timeoutDescription: '剪辑计划生成失败,请重试', + description: `Generating intelligent editing plan... ${retryCount ? 'Retry Time: ' + retryCount : ''}`, + successDescription: 'Editing plan generated successfully.', + timeoutDescription: 'Editing plan generation failed. Please refresh and try again.', timeout: 3 * 60 * 1000 }); // 先停止轮询 @@ -157,8 +161,13 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = setNeedStreamData(false); resolve(true); }); + setIsLoadingGenerateEditPlan(true); try { - await getGenerateEditPlan({ project_id: episodeId }); + const response = await getGenerateEditPlan({ project_id: episodeId }); + if (!response.data.editing_plan) { + throw new Error(response.message); + } + console.error('生成剪辑计划成功'); setIsGenerateEditPlan(true); isLoadedRef.current = 'true'; @@ -168,8 +177,8 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = showEditingNotification({ key: notificationKey, isCompleted: true, - description: '正在生成剪辑计划...', - successDescription: '剪辑计划生成完成', + description: `Generating intelligent editing plan... ${retryCount ? 'Retry Time: ' + retryCount : ''}`, + successDescription: 'Editing plan generated successfully.', timeout: 3000 }); setTimeout(() => { @@ -178,6 +187,7 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = // 触发回调,通知父组件计划生成完成 onEditPlanGenerated?.(); + setIsLoadingGenerateEditPlan(false); } catch (error) { console.error('生成剪辑计划失败:', error); setNeedStreamData(true); @@ -186,30 +196,38 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = // 显示失败通知3秒 showEditingNotification({ key: notificationKey, - description: '正在生成剪辑计划...', - timeoutDescription: '剪辑计划生成失败,请重试', + description: `Generating intelligent editing plan... ${retryCount ? 'Retry Time: ' + retryCount : ''}`, + timeoutDescription: 'Editing plan generation failed. Retrying later.', timeout: 3000 }); setTimeout(() => { notification.destroy(notificationKey); }, 3000); + setIsLoadingGenerateEditPlan(false); } }, [episodeId, onEditPlanGenerated, notificationKey]); const openEditPlan = useCallback(async () => { - window.open(`https://smartcut.movieflow.ai/ai-editor/${episodeId}?token=${token}&user_id=${useid}`, '_target'); + window.open(`${cutUrl}/ai-editor/${episodeId}?token=${token}&user_id=${useid}`, '_target'); }, [episodeId]); useEffect(() => { // 主动触发剪辑 if (canGoToCut && taskObject.currentStage === 'video') { - generateEditPlan(); + generateEditPlan(retryCount); } - }, [canGoToCut, taskObject.currentStage]); + }, [canGoToCut, taskObject.currentStage, retryCount]); + + useEffect(() => { + // 加载剪辑计划结束 并且 失败了 重试 + if (!isLoadingGenerateEditPlan && !isGenerateEditPlan) { + setRetryCount(retryCount + 1); + } + }, [isLoadingGenerateEditPlan, isGenerateEditPlan]); 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]); @@ -252,7 +270,7 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = loadingText.current = LOADING_TEXT_MAP.postProduction('AI-powered video editing in progress…'); } } - if (taskObject.currentStage === 'final_video') { + if (taskObject.currentStage === 'final_video' && taskObject.status !== 'COMPLETED') { loadingText.current = LOADING_TEXT_MAP.postProduction('generating fine-grained video clips...'); } if (taskObject.status === 'COMPLETED') { @@ -498,7 +516,7 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = throw new Error(response.message); } - const { status, data, tags, mode, original_text, title, name } = response.data; + const { status, data, tags, mode, original_text, title, name, final_simple_video, final_video } = response.data; const { current: taskCurrent } = tempTaskObject; @@ -611,6 +629,21 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = } } + // 粗剪 + if (final_simple_video) { + taskCurrent.currentStage = 'final_video'; + taskCurrent.final.url = final_simple_video; + taskCurrent.final.note = 'simple'; + taskCurrent.status = 'COMPLETED'; + } + + if (final_video) { + taskCurrent.currentStage = 'final_video'; + taskCurrent.final.url = final_video; + taskCurrent.final.note = 'final'; + taskCurrent.status = 'COMPLETED'; + } + console.log('---look-taskData', taskCurrent); if (taskCurrent.currentStage === 'script') { @@ -632,7 +665,7 @@ export function useWorkflowData({ onEditPlanGenerated }: UseWorkflowDataProps = }); // 设置是否需要获取流式数据 - setNeedStreamData(status !== 'COMPLETED'); + setNeedStreamData(taskCurrent.status !== 'COMPLETED'); } catch (error) { console.error('初始化失败:', error); 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/hooks/useDeviceType.ts b/hooks/useDeviceType.ts new file mode 100644 index 0000000..a46721f --- /dev/null +++ b/hooks/useDeviceType.ts @@ -0,0 +1,64 @@ +import { useState, useEffect } from 'react'; + +// 定义设备类型枚举 +export enum DeviceType { + MOBILE = 'mobile', // 手机 + TABLET = 'tablet', // 平板 + DESKTOP = 'desktop' // 桌面端 +} + +// 定义屏幕断点 +const BREAKPOINTS = { + MOBILE: 480, // 0-480px 为手机 + TABLET: 1024, // 481-1024px 为平板 + DESKTOP: 1025 // 1025px 及以上为桌面端 +}; + +export function useDeviceType() { + const [deviceType, setDeviceType] = useState(DeviceType.DESKTOP); + const [windowSize, setWindowSize] = useState({ + width: typeof window !== 'undefined' ? window.innerWidth : 0, + height: typeof window !== 'undefined' ? window.innerHeight : 0 + }); + + useEffect(() => { + /** + * 根据窗口宽度判断设备类型 + */ + const getDeviceType = (width: number): DeviceType => { + if (width <= BREAKPOINTS.MOBILE) return DeviceType.MOBILE; + if (width <= BREAKPOINTS.TABLET) return DeviceType.TABLET; + return DeviceType.DESKTOP; + }; + + /** + * 处理窗口大小变化 + */ + const handleResize = () => { + const width = window.innerWidth; + const height = window.innerHeight; + + setWindowSize({ width, height }); + setDeviceType(getDeviceType(width)); + }; + + // 初始化设备类型 + handleResize(); + + // 添加窗口大小变化监听 + window.addEventListener('resize', handleResize); + + // 清理监听器 + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + return { + deviceType, + windowSize, + isMobile: deviceType === DeviceType.MOBILE, + isTablet: deviceType === DeviceType.TABLET, + isDesktop: deviceType === DeviceType.DESKTOP + }; +} \ No newline at end of file diff --git a/lib/auth.ts b/lib/auth.ts index f6eb482..fb3de7c 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -41,40 +41,6 @@ export const loginUser = async (email: string, password: string) => { } }; -/** - * 鉴权检查 - */ -export const checkAuth = async (): Promise => { - const token = getToken(); - if (!token) { - return false; - } - - try { - const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/admin/login/auth`, { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'X-EASE-ADMIN-TOKEN': token, - }, - }); - - const data = await response.json(); - return true; - // if (data.code === '401' || data.status === 401) { - // // Token无效,清除本地存储 - // // clearAuthData(); - // // return false; - // } - - // return data.code === '200' && data.status === 200; - } catch (error) { - console.error('Auth check failed:', error); - return false; - } -}; - /** * 获取token */ 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"] } diff --git a/utils/notifications.tsx b/utils/notifications.tsx index a516df2..a44bbcd 100644 --- a/utils/notifications.tsx +++ b/utils/notifications.tsx @@ -58,15 +58,33 @@ const btnStyle = { * 显示积分不足通知 * @description 在右上角显示一个带有充值链接的积分不足提醒 */ -export const showInsufficientPointsNotification = () => { +export const showInsufficientPointsNotification = (detail?: { + current_balance?: number; + required_tokens?: number; + message?: string; +}) => { + // 生成唯一的 key + const key = `insufficient-points-${Date.now()}`; + notification.warning({ + key, // 添加唯一的 key message: null, description: (

Insufficient credits reminder

-

Your credits are insufficient, please upgrade to continue.

+

+ {detail?.message || 'Your credits are insufficient, please upgrade to continue.'} + {detail?.current_balance !== undefined && detail?.required_tokens !== undefined && ( + <> +
+ + Current balance: {detail.current_balance} / Required: {detail.required_tokens} + + + )} +

), - duration: 5, + duration: 0, placement: 'topRight', style: darkGlassStyle, className: 'dark-glass-notification',