forked from 77media/video-flow
Merge branch 'dev' of https://git.qikongjian.com/77media/video-flow into dev
This commit is contained in:
commit
055732593a
@ -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
|
||||
NEXT_PUBLIC_ERROR_CONFIG = 0.5
|
||||
@ -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
|
||||
|
||||
@ -363,6 +363,10 @@ export interface VideoFlowProjectResponse {
|
||||
last_message: string;
|
||||
/** 项目内容 */
|
||||
data: ProjectContentData;
|
||||
/** 最终简单视频 */
|
||||
final_simple_video: string;
|
||||
/** 最终视频 */
|
||||
final_video: string;
|
||||
}
|
||||
/**
|
||||
* 新角色列表项接口
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { message } from "antd";
|
||||
import { debounce } from "lodash";
|
||||
|
||||
/**
|
||||
@ -8,6 +7,7 @@ const HTTP_ERROR_MESSAGES: Record<number, string> = {
|
||||
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<number, () => 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];
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -292,8 +292,8 @@ export const getRunningStreamData = async (data: {
|
||||
};
|
||||
|
||||
// 获取 生成剪辑计划 接口
|
||||
export const getGenerateEditPlan = async (data: GenerateEditPlanRequest): Promise<ApiResponse<GenerateEditPlanResponse>> => {
|
||||
return post<ApiResponse<GenerateEditPlanResponse>>("/edit-plan/generate-by-project", data);
|
||||
export const getGenerateEditPlan = async (data: GenerateEditPlanRequest): Promise<ApiResponse<GenerateEditPlanResponseData>> => {
|
||||
return post<ApiResponse<GenerateEditPlanResponseData>>("/edit-plan/generate-by-project", data);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<TopBar collapsed={true} />
|
||||
<OAuthCallbackHandler />
|
||||
<HomePage2 />
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -172,4 +172,13 @@ export interface StoryTemplateEntity {
|
||||
/** 道具照片URL */
|
||||
photo_url: string;
|
||||
}[];
|
||||
/** 自由输入文字 */
|
||||
freeInputItem?: {
|
||||
/** 用户提示,提示给用户需要输入什么内容 */
|
||||
user_tips: string;
|
||||
/** 约束,可选,用于传给ai,让ai去拦截用户不符合约束的输入内容 */
|
||||
constraints: string;
|
||||
/** 自由输入文字 */
|
||||
free_input_text: string;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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 = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 角色配置区域 */}
|
||||
{selectedTemplate?.storyRole &&
|
||||
selectedTemplate.storyRole.length > 0 && (
|
||||
@ -755,7 +790,29 @@ const RenderTemplateStoryMode = ({
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className=" absolute -bottom-8 right-0">
|
||||
<div className=" absolute -bottom-8 right-0 w-full flex items-center justify-end gap-2">
|
||||
{/** 自由输入文字 */}
|
||||
{(selectedTemplate?.freeInputItem) && (
|
||||
<div className="py-2 flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={selectedTemplate?.freeInputItem?.free_input_text || ""}
|
||||
placeholder="How is coffee made?"
|
||||
className="w-full px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
|
||||
onChange={(e) => {
|
||||
// 更新自由输入文字字段
|
||||
const updatedTemplate = {
|
||||
...selectedTemplate!,
|
||||
freeInputItem: {
|
||||
...selectedTemplate!.freeInputItem,
|
||||
free_input_text: e.target.value
|
||||
}
|
||||
};
|
||||
setSelectedTemplate(updatedTemplate as StoryTemplateEntity);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ActionButton
|
||||
isCreating={isTemplateCreating || localLoading > 0}
|
||||
handleCreateVideo={handleConfirm}
|
||||
|
||||
@ -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';
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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 (
|
||||
<div className=" min-h-screen bg-background">
|
||||
<TopBar collapsed={sidebarCollapsed} />
|
||||
<Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />
|
||||
<div className="h-[calc(100vh-4rem)] top-[4rem] fixed right-0 bottom-0 z-[999]" style={{
|
||||
left: sidebarCollapsed ? '4rem' : '16rem',
|
||||
width: sidebarCollapsed ? 'calc(100vw - 4rem)' : 'calc(100vw - 16rem)'
|
||||
}}>
|
||||
<TopBar collapsed={sidebarCollapsed} isDesktop={isDesktop} />
|
||||
{isDesktop && <Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />}
|
||||
<div
|
||||
className="h-[calc(100vh-4rem)] top-[4rem] fixed right-0 bottom-0 z-[999] px-4"
|
||||
style={getLayoutStyles()}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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<HTMLDivElement>(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")
|
||||
}}
|
||||
>
|
||||
<div className="h-full flex items-center justify-between pr-6 pl-4">
|
||||
<div className="h-full flex items-center justify-between px-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div
|
||||
className={`flex items-center cursor-pointer space-x-1 link-logo roll event-on`}
|
||||
@ -239,7 +240,11 @@ export function TopBar({ collapsed }: { collapsed: boolean }) {
|
||||
)}
|
||||
|
||||
{/* Notifications */}
|
||||
{/* <Button variant="ghost" size="sm" onClick={() => showQueueNotification(3, 10)}>
|
||||
{/* <Button variant="ghost" size="sm" onClick={() => showInsufficientPointsNotification({
|
||||
current_balance: 20,
|
||||
required_tokens: 100,
|
||||
message: 'Insufficient points'
|
||||
})}>
|
||||
<Bell className="h-4 w-4" />
|
||||
</Button> */}
|
||||
|
||||
@ -273,7 +278,9 @@ export function TopBar({ collapsed }: { collapsed: boolean }) {
|
||||
padding: "unset !important",
|
||||
}}
|
||||
>
|
||||
<div className="h-10 w-10 rounded-full bg-[#C73BFF] flex items-center justify-center text-white font-semibold">
|
||||
<div
|
||||
className={`${isDesktop ? "h-10 w-10" : "h-8 w-8"} rounded-full bg-[#C73BFF] flex items-center justify-center text-white font-semibold`}
|
||||
>
|
||||
{currentUser.username ? currentUser.username.charAt(0) : "MF"}
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
@ -287,14 +287,21 @@ export default function CreateToVideo2() {
|
||||
{/* 加载更多指示器 */}
|
||||
{isLoadingMore && (
|
||||
<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" />
|
||||
<span className="text-white/90 font-medium">Loading more projects...</span>
|
||||
</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>
|
||||
|
||||
{/* 视频工具组件 - 使用独立组件 */}
|
||||
|
||||
@ -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: () => {
|
||||
|
||||
@ -92,9 +92,11 @@ export const AIEditingIframe = React.forwardRef<AIEditingIframeHandle, AIEditing
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(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<AIEditingIframeHandle, AIEditing
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
// 验证消息来源
|
||||
if (!event.origin.includes('smartcut.movieflow.ai')) {
|
||||
if (!event.origin.includes(cutUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -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 (
|
||||
<div
|
||||
|
||||
@ -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;
|
||||
@ -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);
|
||||
|
||||
@ -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 (
|
||||
<Provider store={store}>
|
||||
<ThemeProvider
|
||||
@ -30,8 +31,23 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
||||
<AuthGuard>
|
||||
{children}
|
||||
</AuthGuard>
|
||||
<Toaster />
|
||||
<OAuthCallbackHandler />
|
||||
<Toaster
|
||||
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 />}
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
|
||||
@ -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<ScriptRendererProps> = ({ 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<ScriptRendererProps> = ({ 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<ScriptRendererProps> = ({ 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!');
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
@ -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修改的数据
|
||||
|
||||
@ -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 { 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<any, ShotsEditorProps>(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();
|
||||
|
||||
@ -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<
|
||||
{/* 人物替换按钮 */}
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
msg.error('No permission!');
|
||||
window.msg.error('No permission!');
|
||||
return;
|
||||
handleScan()
|
||||
}}
|
||||
@ -504,7 +503,7 @@ export const ShotTabContent = forwardRef<
|
||||
</motion.button> */}
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
msg.error('No permission!');
|
||||
window.msg.error('No permission!');
|
||||
return;
|
||||
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 };
|
||||
64
hooks/useDeviceType.ts
Normal file
64
hooks/useDeviceType.ts
Normal file
@ -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>(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
|
||||
};
|
||||
}
|
||||
34
lib/auth.ts
34
lib/auth.ts
@ -41,40 +41,6 @@ export const loginUser = async (email: string, password: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 鉴权检查
|
||||
*/
|
||||
export const checkAuth = async (): Promise<boolean> => {
|
||||
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
|
||||
*/
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
@ -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: (
|
||||
<div data-alt="insufficient-points-notification" style={{ minWidth: '280px' }}>
|
||||
<h3 style={messageStyle}>
|
||||
Insufficient credits reminder
|
||||
</h3>
|
||||
<p style={descriptionStyle}>Your credits are insufficient, please upgrade to continue.</p>
|
||||
<p style={descriptionStyle}>
|
||||
{detail?.message || 'Your credits are insufficient, please upgrade to continue.'}
|
||||
{detail?.current_balance !== undefined && detail?.required_tokens !== undefined && (
|
||||
<>
|
||||
<br />
|
||||
<span style={{ color: 'rgba(255, 255, 255, 0.85)' }}>
|
||||
Current balance: {detail.current_balance} / Required: {detail.required_tokens}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.href = '/pricing'}
|
||||
style={btnStyle}
|
||||
@ -76,7 +94,7 @@ export const showInsufficientPointsNotification = () => {
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
duration: 5,
|
||||
duration: 0,
|
||||
placement: 'topRight',
|
||||
style: darkGlassStyle,
|
||||
className: 'dark-glass-notification',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user