增加代码编辑器表单组件
This commit is contained in:
parent
7c6275c03a
commit
97f6e8c8e2
167
frontend/src/pages/Maintenance/index.tsx
Normal file
167
frontend/src/pages/Maintenance/index.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Settings, RefreshCw, Clock, AlertCircle } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { maintenanceDetector } from '@/services/maintenanceDetector';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 维护模式页面
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 显示系统维护状态
|
||||||
|
* 2. 后台自动检测服务恢复(静默)
|
||||||
|
* 3. 支持手动重试连接
|
||||||
|
*/
|
||||||
|
const MaintenancePage = () => {
|
||||||
|
const [checking, setChecking] = useState(false);
|
||||||
|
const [lastCheckTime, setLastCheckTime] = useState<Date>(new Date());
|
||||||
|
|
||||||
|
// 后台静默检测(每30秒一次,不打扰用户)
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setLastCheckTime(new Date());
|
||||||
|
// 静默检测,不显示loading状态
|
||||||
|
maintenanceDetector.checkRecovery();
|
||||||
|
}, 30000); // 30秒检测一次
|
||||||
|
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 手动检测服务器状态
|
||||||
|
const handleManualCheck = async () => {
|
||||||
|
setChecking(true);
|
||||||
|
setLastCheckTime(new Date());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isRecovered = await maintenanceDetector.checkRecovery();
|
||||||
|
|
||||||
|
if (!isRecovered) {
|
||||||
|
console.log('服务尚未恢复,请稍后再试');
|
||||||
|
}
|
||||||
|
// 如果恢复了,maintenanceDetector 会自动跳转
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检测服务恢复失败:', error);
|
||||||
|
} finally {
|
||||||
|
setChecking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取维护开始时间(用于显示已维护时长)
|
||||||
|
const getMaintenanceDuration = () => {
|
||||||
|
const startTime = maintenanceDetector.getMaintenanceStartTime();
|
||||||
|
if (!startTime) return '未知';
|
||||||
|
|
||||||
|
const duration = Math.floor((Date.now() - startTime) / 1000 / 60);
|
||||||
|
return duration < 1 ? '刚刚开始' : `${duration} 分钟`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50">
|
||||||
|
<div className="text-center p-8 max-w-2xl w-full mx-4">
|
||||||
|
{/* 动画图标 */}
|
||||||
|
<div className="mb-8 relative">
|
||||||
|
<Settings
|
||||||
|
className="w-20 h-20 mx-auto text-blue-500"
|
||||||
|
style={{
|
||||||
|
animation: 'spin 3s linear infinite',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div
|
||||||
|
className="w-28 h-28 border-4 border-blue-200 rounded-full opacity-20"
|
||||||
|
style={{
|
||||||
|
animation: 'ping 2s cubic-bezier(0, 0, 0.2, 1) infinite',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标题 */}
|
||||||
|
<h1 className="text-5xl font-bold text-gray-800 mb-4">系统升级中</h1>
|
||||||
|
|
||||||
|
<p className="text-gray-600 text-xl mb-3 leading-relaxed">
|
||||||
|
系统暂时不可用,请稍后再试
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-400 mb-12">
|
||||||
|
可能原因:系统升级、服务器维护或临时故障
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 信息卡片 */}
|
||||||
|
<div className="bg-white rounded-2xl p-8 shadow-xl backdrop-blur-sm bg-opacity-90 space-y-6">
|
||||||
|
{/* 状态信息 */}
|
||||||
|
<div className="flex items-center justify-center space-x-8 text-sm">
|
||||||
|
<div className="flex items-center space-x-2 text-gray-600">
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
<span>已等待 {getMaintenanceDuration()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 text-gray-600">
|
||||||
|
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
||||||
|
<span>后台自动检测中</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 重试按钮 */}
|
||||||
|
<Button
|
||||||
|
onClick={handleManualCheck}
|
||||||
|
disabled={checking}
|
||||||
|
className="w-full h-12 text-lg"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{checking ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="mr-2 h-5 w-5 animate-spin" />
|
||||||
|
检测中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="mr-2 h-5 w-5" />
|
||||||
|
手动重试
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 说明文字 */}
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
系统会自动检测服务恢复,恢复后将自动刷新页面
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 提示信息 */}
|
||||||
|
<div className="mt-8 bg-blue-50 rounded-xl p-6 border border-blue-100">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<AlertCircle className="w-5 h-5 text-blue-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-left space-y-2 text-sm text-gray-600">
|
||||||
|
<p className="font-medium text-gray-700">您可以:</p>
|
||||||
|
<ul className="space-y-1 list-disc list-inside">
|
||||||
|
<li>稍等片刻,页面会自动恢复</li>
|
||||||
|
<li>点击"手动重试"按钮尝试重新连接</li>
|
||||||
|
<li>如长时间未恢复,请联系系统管理员</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 添加动画样式 */}
|
||||||
|
<style>{`
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ping {
|
||||||
|
75%, 100% {
|
||||||
|
transform: scale(1.5);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MaintenancePage;
|
||||||
@ -14,6 +14,9 @@ const Forbidden = lazy(() => import('@/pages/Error/403'));
|
|||||||
const NotFound = lazy(() => import('@/pages/Error/404'));
|
const NotFound = lazy(() => import('@/pages/Error/404'));
|
||||||
const NoPermission = lazy(() => import('@/pages/Error/NoPermission'));
|
const NoPermission = lazy(() => import('@/pages/Error/NoPermission'));
|
||||||
|
|
||||||
|
// 维护页面
|
||||||
|
const Maintenance = lazy(() => import('@/pages/Maintenance'));
|
||||||
|
|
||||||
// 表单设计器测试页面(写死的路由)
|
// 表单设计器测试页面(写死的路由)
|
||||||
const FormDesignerTest = lazy(() => import('../pages/FormDesigner'));
|
const FormDesignerTest = lazy(() => import('../pages/FormDesigner'));
|
||||||
|
|
||||||
@ -78,6 +81,15 @@ export const createDynamicRouter = () => {
|
|||||||
path: '/login',
|
path: '/login',
|
||||||
element: <Login />,
|
element: <Login />,
|
||||||
},
|
},
|
||||||
|
// 维护页面(独立路由,无需布局和权限验证)
|
||||||
|
{
|
||||||
|
path: '/maintenance',
|
||||||
|
element: (
|
||||||
|
<Suspense fallback={<RouteLoading />}>
|
||||||
|
<Maintenance />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: <BasicLayout />,
|
element: <BasicLayout />,
|
||||||
|
|||||||
184
frontend/src/services/maintenanceDetector.ts
Normal file
184
frontend/src/services/maintenanceDetector.ts
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* 维护模式检测服务
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 检测后端服务是否可用
|
||||||
|
* 2. 连续失败达到阈值时判定为维护模式
|
||||||
|
* 3. 自动轮询检测服务恢复
|
||||||
|
*/
|
||||||
|
|
||||||
|
class MaintenanceDetector {
|
||||||
|
private failureCount = 0;
|
||||||
|
private readonly FAILURE_THRESHOLD = 3; // 连续失败3次判定为维护
|
||||||
|
private readonly RESET_INTERVAL = 60000; // 60秒后重置计数
|
||||||
|
private resetTimer: NodeJS.Timeout | null = null;
|
||||||
|
private recoveryCheckTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录请求失败
|
||||||
|
*/
|
||||||
|
recordFailure() {
|
||||||
|
this.failureCount++;
|
||||||
|
console.log(`[MaintenanceDetector] 请求失败次数: ${this.failureCount}/${this.FAILURE_THRESHOLD}`);
|
||||||
|
|
||||||
|
// 清除之前的重置计时器
|
||||||
|
if (this.resetTimer) {
|
||||||
|
clearTimeout(this.resetTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 60秒后自动重置计数(避免误判网络抖动)
|
||||||
|
this.resetTimer = setTimeout(() => {
|
||||||
|
console.log('[MaintenanceDetector] 重置失败计数');
|
||||||
|
this.failureCount = 0;
|
||||||
|
}, this.RESET_INTERVAL);
|
||||||
|
|
||||||
|
// 达到阈值,判定为维护模式
|
||||||
|
if (this.failureCount >= this.FAILURE_THRESHOLD) {
|
||||||
|
this.enterMaintenanceMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录请求成功
|
||||||
|
*/
|
||||||
|
recordSuccess() {
|
||||||
|
if (this.failureCount > 0) {
|
||||||
|
console.log('[MaintenanceDetector] 请求成功,重置失败计数');
|
||||||
|
this.failureCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除重置计时器
|
||||||
|
if (this.resetTimer) {
|
||||||
|
clearTimeout(this.resetTimer);
|
||||||
|
this.resetTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进入维护模式
|
||||||
|
*/
|
||||||
|
private enterMaintenanceMode() {
|
||||||
|
console.log('[MaintenanceDetector] ========== 进入维护模式 ==========');
|
||||||
|
console.log('[MaintenanceDetector] 当前路径:', window.location.pathname);
|
||||||
|
|
||||||
|
// 标记维护状态到 sessionStorage
|
||||||
|
sessionStorage.setItem('maintenance_mode', 'true');
|
||||||
|
sessionStorage.setItem('maintenance_start_time', Date.now().toString());
|
||||||
|
console.log('[MaintenanceDetector] sessionStorage 已设置');
|
||||||
|
|
||||||
|
// 跳转到维护页面
|
||||||
|
if (window.location.pathname !== '/maintenance') {
|
||||||
|
console.log('[MaintenanceDetector] 准备跳转到 /maintenance');
|
||||||
|
console.log('[MaintenanceDetector] 使用 window.location.replace 跳转...');
|
||||||
|
|
||||||
|
// 使用 setTimeout 确保跳转执行
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.replace('/maintenance');
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
console.log('[MaintenanceDetector] 已在维护页面,无需跳转');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始轮询检测恢复
|
||||||
|
this.startRecoveryCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定期检测服务恢复
|
||||||
|
*/
|
||||||
|
private startRecoveryCheck() {
|
||||||
|
// 避免重复启动
|
||||||
|
if (this.recoveryCheckTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[MaintenanceDetector] 开始轮询检测服务恢复(每30秒)');
|
||||||
|
|
||||||
|
this.recoveryCheckTimer = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
// 尝试请求一个轻量级接口
|
||||||
|
// 使用原生fetch避免触发axios拦截器
|
||||||
|
const response = await fetch('/api/health', {
|
||||||
|
method: 'GET',
|
||||||
|
cache: 'no-cache',
|
||||||
|
signal: AbortSignal.timeout(3000)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 只有返回 200-299 才算真正恢复
|
||||||
|
// 500+ 错误不算恢复
|
||||||
|
if (response.ok) {
|
||||||
|
// 服务恢复
|
||||||
|
console.log('[MaintenanceDetector] 服务已恢复!');
|
||||||
|
this.exitMaintenanceMode();
|
||||||
|
} else {
|
||||||
|
console.log(`[MaintenanceDetector] 服务仍不可用 (状态码: ${response.status})`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('[MaintenanceDetector] 服务检测失败,继续等待...');
|
||||||
|
}
|
||||||
|
}, 30000); // 每30秒检查一次
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出维护模式
|
||||||
|
*/
|
||||||
|
private exitMaintenanceMode() {
|
||||||
|
console.log('[MaintenanceDetector] 退出维护模式');
|
||||||
|
|
||||||
|
// 清除维护状态
|
||||||
|
sessionStorage.removeItem('maintenance_mode');
|
||||||
|
sessionStorage.removeItem('maintenance_start_time');
|
||||||
|
|
||||||
|
// 停止轮询
|
||||||
|
if (this.recoveryCheckTimer) {
|
||||||
|
clearInterval(this.recoveryCheckTimer);
|
||||||
|
this.recoveryCheckTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置失败计数
|
||||||
|
this.failureCount = 0;
|
||||||
|
|
||||||
|
// 刷新页面(返回应用)
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动检测恢复(供维护页面调用)
|
||||||
|
*/
|
||||||
|
async checkRecovery(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/health', {
|
||||||
|
method: 'GET',
|
||||||
|
cache: 'no-cache',
|
||||||
|
signal: AbortSignal.timeout(3000)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 只有返回 200-299 才算真正恢复
|
||||||
|
if (response.ok) {
|
||||||
|
this.exitMaintenanceMode();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取维护开始时间
|
||||||
|
*/
|
||||||
|
getMaintenanceStartTime(): number | null {
|
||||||
|
const startTime = sessionStorage.getItem('maintenance_start_time');
|
||||||
|
return startTime ? parseInt(startTime, 10) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否在维护模式
|
||||||
|
*/
|
||||||
|
isInMaintenanceMode(): boolean {
|
||||||
|
return sessionStorage.getItem('maintenance_mode') === 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例
|
||||||
|
export const maintenanceDetector = new MaintenanceDetector();
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import axios, {AxiosRequestConfig, AxiosResponse} from 'axios';
|
import axios, {AxiosRequestConfig, AxiosResponse} from 'axios';
|
||||||
import {toast} from '@/components/ui/use-toast';
|
import {toast} from '@/components/ui/use-toast';
|
||||||
import { API_BASE_URL, isDev } from '@/config/env';
|
import { API_BASE_URL, isDev } from '@/config/env';
|
||||||
|
import { maintenanceDetector } from '@/services/maintenanceDetector';
|
||||||
|
|
||||||
export interface Response<T = any> {
|
export interface Response<T = any> {
|
||||||
code: number;
|
code: number;
|
||||||
@ -47,6 +48,9 @@ const createRequestConfig = (options?: Partial<RequestOptions>): RequestOptions
|
|||||||
};
|
};
|
||||||
|
|
||||||
const responseHandler = (response: AxiosResponse<Response<any>>) => {
|
const responseHandler = (response: AxiosResponse<Response<any>>) => {
|
||||||
|
// 请求成功,记录到维护检测器
|
||||||
|
maintenanceDetector.recordSuccess();
|
||||||
|
|
||||||
const result = response.data;
|
const result = response.data;
|
||||||
if (result.success && result.code === 200) {
|
if (result.success && result.code === 200) {
|
||||||
return result.data;
|
return result.data;
|
||||||
@ -65,12 +69,12 @@ const responseHandler = (response: AxiosResponse<Response<any>>) => {
|
|||||||
|
|
||||||
|
|
||||||
const errorHandler = (error: any) => {
|
const errorHandler = (error: any) => {
|
||||||
|
// 检测网络错误(后端完全不可达)
|
||||||
if (!error.response) {
|
if (!error.response) {
|
||||||
toast({
|
// 记录失败到维护检测器
|
||||||
title: '网络错误',
|
maintenanceDetector.recordFailure();
|
||||||
description: '网络连接异常,请检查网络',
|
|
||||||
variant: 'destructive'
|
// 不显示toast,因为会跳转到维护页面
|
||||||
});
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,6 +83,12 @@ const errorHandler = (error: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const status = error.response.status;
|
const status = error.response.status;
|
||||||
|
|
||||||
|
// 检测严重的服务器错误(500+),可能是服务器故障或维护
|
||||||
|
if (status >= 500) {
|
||||||
|
maintenanceDetector.recordFailure();
|
||||||
|
// 继续执行下面的错误处理,显示具体的错误信息
|
||||||
|
}
|
||||||
let errorMessage = '';
|
let errorMessage = '';
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 401:
|
case 401:
|
||||||
@ -139,6 +149,16 @@ const errorHandler = (error: any) => {
|
|||||||
|
|
||||||
request.interceptors.request.use(
|
request.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
|
// 检查是否处于维护模式
|
||||||
|
if (maintenanceDetector.isInMaintenanceMode()) {
|
||||||
|
// 如果在维护模式且当前不在维护页面,跳转
|
||||||
|
if (window.location.pathname !== '/maintenance') {
|
||||||
|
window.location.replace('/maintenance');
|
||||||
|
}
|
||||||
|
// 取消请求,避免继续发送
|
||||||
|
return Promise.reject(new Error('System in maintenance mode'));
|
||||||
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user