权限验证

This commit is contained in:
北枳 2025-08-27 20:11:08 +08:00
parent e7718340c4
commit c56043d85c
8 changed files with 260 additions and 48 deletions

View File

@ -16,14 +16,32 @@ const HTTP_ERROR_MESSAGES: Record<number, string> = {
503: "Service temporarily unavailable, please try again later.",
504: "Gateway timeout, please try again later.",
};
/**
*
*/
const DEFAULT_ERROR_MESSAGE =
"Please try again if the network is abnormal. If it happens again, please contact us.";
const DEFAULT_ERROR_MESSAGE = "网络异常,请重试。如果问题持续存在,请联系我们。";
/**
*
*
*/
const ERROR_HANDLERS: Record<number, () => void> = {
401: () => {
// 清除本地存储的 token
localStorage.removeItem('token');
// 跳转到登录页面
window.location.href = '/login';
},
403: () => {
// 显示积分不足通知
import('../utils/notifications').then(({ showInsufficientPointsNotification }) => {
showInsufficientPointsNotification();
});
}
};
/**
*
* @param code - HTTP错误码
* @param customMessage -
*/
@ -31,7 +49,19 @@ export const errorHandle = debounce(
(code: number, customMessage?: string): void => {
const errorMessage =
customMessage || HTTP_ERROR_MESSAGES[code] || DEFAULT_ERROR_MESSAGE;
message.error(errorMessage);
// 显示错误提示
message.error({
content: errorMessage,
duration: 3,
className: 'custom-error-message'
});
// 执行特殊错误码的处理函数
const handler = ERROR_HANDLERS[code];
if (handler) {
handler();
}
},
100
);

View File

@ -2,6 +2,25 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosR
import { message } from "antd";
import { BASE_URL } from './constants'
import { errorHandle } from './errorHandle';
/**
*
* @param error -
* @param defaultMessage -
*/
const handleRequestError = (error: any, defaultMessage: string = '请求失败') => {
if (error.response) {
const { status, data } = error.response;
const errorMessage = data?.message || defaultMessage;
errorHandle(status, errorMessage);
} else if (error.request) {
// 请求已发出但没有收到响应
errorHandle(0, '网络请求失败,请检查网络连接');
} else {
// 请求配置出错
errorHandle(0, error.message || defaultMessage);
}
};
// 创建 axios 实例
const request: AxiosInstance = axios.create({
baseURL: BASE_URL, // 设置基础URL
@ -29,23 +48,32 @@ request.interceptors.request.use(
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse) => {
// 直接返回响应数据
if (response.data?.code !=0) {
// TODO 暂时固定报错信息,后续根据后端返回的错误码进行处理
errorHandle(0);
// 检查业务状态码
if (response.data?.code !== 0) {
// 处理业务层面的错误
const businessCode = response.data?.code;
const errorMessage = response.data?.message;
// 特殊处理 401 和 403 业务状态码
if (businessCode === 401) {
errorHandle(401, errorMessage);
return Promise.reject(new Error(errorMessage));
}
if (businessCode === 403) {
errorHandle(403, errorMessage);
return Promise.reject(new Error(errorMessage));
}
// 其他业务错误
errorHandle(0, errorMessage);
return Promise.reject(new Error(errorMessage));
}
return response.data;
},
(error) => {
if (error.response) {
errorHandle(error.response.status);
} else if (error.request) {
// 请求已发出但没有收到响应
errorHandle(0);
} else {
// 检修
console.error(error);
}
handleRequestError(error);
return Promise.reject(error);
}
);
@ -86,8 +114,23 @@ export async function streamJsonPost<T = any>(
body: JSON.stringify(body),
});
// 处理 HTTP 错误状态
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
const error = {
response: {
status: response.status,
data: { message: await response.text().then(text => {
try {
const data = JSON.parse(text);
return data.message || `HTTP error! status: ${response.status}`;
} catch {
return `HTTP error! status: ${response.status}`;
}
})}
}
};
handleRequestError(error);
throw error;
}
if (!response.body) {
@ -207,7 +250,8 @@ export const stream = async <T>({
const response = await request(config);
onComplete?.();
return response;
} catch (error) {
} catch (error: any) {
handleRequestError(error, '流式请求失败');
onError?.(error);
throw error;
}
@ -239,8 +283,9 @@ export const downloadStream = async (
window.URL.revokeObjectURL(downloadUrl);
return response;
} catch (error) {
} catch (error: any) {
console.error('文件下载失败:', error);
handleRequestError(error, '文件下载失败');
throw error;
}
};

View File

@ -239,3 +239,7 @@ body {
.animate-fade-in {
animation: fade-in 0.2s ease-out forwards;
}
.ant-notification-notice-wrapper {
background: transparent !important;
}

View File

@ -124,6 +124,8 @@ export function MessageRenderer({ msg }: MessageRendererProps) {
);
case "progress":
return <ProgressBar key={idx} value={b.value} total={b.total} label={b.label} />;
case "link":
return <a key={idx} href={b.url} className="underline">{b.text}</a>;
default:
return null;
}

View File

@ -25,7 +25,7 @@ const EMPTY_MESSAGES: RealApiMessage[] = [
role: 'assistant',
content: JSON.stringify([{
type: 'text',
content: '🌟欢迎来到 MovieFlow 🎬✨\n快把您的创意告诉我吧💡\n我是您的专属AI小伙伴🤖可以帮您\n🎭 生成专属演员形象\n📽 搭建场景 & 分镜\n🎞 完成整部视频创作\n\n一起开启奇妙的创作之旅吧❤️'
content: '🌟Welcome to MovieFlow 🎬✨\nTell me your idea💡\nI am your AI assistant🤖, I can help you:\n🎭 Generate actor images\n📽 Generate scene & shot sketches\n🎞 Complete video creation\n\nLet\'s start our creative journey together!❤️'
}]),
created_at: new Date().toISOString(),
function_name: undefined,
@ -35,6 +35,19 @@ const EMPTY_MESSAGES: RealApiMessage[] = [
}
];
// 用户积分不足消息
const NoEnoughCreditsMessageBlocks: MessageBlock[] = [
{
type: 'text',
text: 'Insufficient credits.'
},
{
type: 'link',
text: 'Upgrade to continue.',
url: '/pricing'
}
];
/**
*
*/
@ -195,7 +208,7 @@ function transformSystemMessage(
*/
function transformMessage(apiMessage: RealApiMessage): ChatMessage {
try {
const { id, role, content, created_at, function_name, custom_data, status, intent_type } = apiMessage;
const { id, role, content, created_at, function_name, custom_data, status, intent_type, error_message } = apiMessage;
let message: ChatMessage = {
id: id ? id.toString() : Date.now().toString(),
role: role,
@ -205,6 +218,9 @@ function transformMessage(apiMessage: RealApiMessage): ChatMessage {
status: status || 'success',
};
if (error_message && error_message === 'no enough credits') {
message.blocks = NoEnoughCreditsMessageBlocks;
} else {
if (role === 'assistant' || role === 'user') {
try {
const contentObj = JSON.parse(content);
@ -218,6 +234,8 @@ function transformMessage(apiMessage: RealApiMessage): ChatMessage {
message.blocks.push({ type: "video", url: c.content });
} else if (c.type === "audio") {
message.blocks.push({ type: "audio", url: c.content });
} else if (c.type === "link") {
message.blocks.push({ type: "link", text: c.content, url: c.url || '' });
}
});
} catch (error) {
@ -230,6 +248,7 @@ function transformMessage(apiMessage: RealApiMessage): ChatMessage {
} else {
message.blocks.push({ type: "text", text: content });
}
}
// 如果没有有效的 blocks至少添加一个文本块
if (message.blocks.length === 0) {

View File

@ -7,7 +7,8 @@ export type MessageBlock =
| { type: "image"; url: string; alt?: string }
| { type: "video"; url: string; poster?: string }
| { type: "audio"; url: string }
| { type: "progress"; value: number; total?: number; label?: string };
| { type: "progress"; value: number; total?: number; label?: string }
| { type: "link"; text: string; url: string };
export interface ChatMessage {
id: string;
@ -127,6 +128,7 @@ export interface ShotVideoGeneration {
export interface ApiMessageContent {
type: ContentType;
content: string;
url?: string;
}
export interface RealApiMessage {
@ -138,4 +140,5 @@ export interface RealApiMessage {
custom_data?: ProjectInit | ScriptSummary | CharacterGeneration | SketchGeneration | ShotSketchGeneration | ShotVideoGeneration;
status: MessageStatus;
intent_type: IntentType;
error_message?: string;
}

View File

@ -25,6 +25,7 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
const menuRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const currentUser = localStorage.getItem('currentUser');
const [openModal, setOpenModal] = React.useState(false);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@ -102,9 +103,9 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
</Button>
{/* Notifications */}
<Button variant="ghost" size="sm">
{/* <Button variant="ghost" size="sm" onClick={() => setOpenModal(true)}>
<Bell className="h-4 w-4" />
</Button>
</Button> */}
{/* Theme Toggle */}
{/* <Button

108
utils/notifications.tsx Normal file
View File

@ -0,0 +1,108 @@
import { notification } from 'antd';
import { useRouter } from 'next/router';
type NotificationType = 'success' | 'info' | 'warning' | 'error';
const darkGlassStyle = {
background: 'rgba(30, 32, 40, 0.95)',
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(255, 255, 255, 0.08)',
borderRadius: '8px',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)',
padding: '12px 16px',
};
const messageStyle = {
fontSize: '13px',
fontWeight: 500,
color: '#ffffff',
marginBottom: '6px',
display: 'flex',
alignItems: 'center',
gap: '6px',
};
const iconStyle = {
color: '#F6B266', // 警告图标颜色
background: 'rgba(246, 178, 102, 0.15)',
padding: '4px',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
};
const descriptionStyle = {
fontSize: '12px',
color: 'rgba(255, 255, 255, 0.65)',
marginBottom: '12px',
lineHeight: '1.5',
};
const btnStyle = {
color: 'rgb(250 173 20 / 90%)',
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: 0,
fontSize: '12px',
fontWeight: 500,
textDecoration: 'underline',
textUnderlineOffset: '2px',
textDecorationColor: 'rgb(250 173 20 / 60%)',
transition: 'all 0.2s ease',
};
/**
*
* @description
*/
export const showInsufficientPointsNotification = () => {
notification.warning({
message: null,
description: (
<div data-alt="insufficient-points-notification" style={{ minWidth: '280px' }}>
<h3 style={messageStyle}>
</h3>
<p style={descriptionStyle}>使</p>
<button
onClick={() => window.location.href = '/pricing'}
style={btnStyle}
data-alt="recharge-button"
>
/
</button>
</div>
),
duration: 5,
placement: 'topRight',
style: darkGlassStyle,
className: 'dark-glass-notification',
closeIcon: (
<button
className="hover:text-white"
style={{
background: 'transparent',
border: 'none',
padding: '2px',
cursor: 'pointer',
color: 'rgba(255, 255, 255, 0.45)',
transition: 'color 0.2s ease',
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
),
});
};
/**
*
*/
notification.config({
maxCount: 3, // 最多同时显示3个通知
});