增加代码编辑器表单组件

This commit is contained in:
dengqichen 2025-11-18 23:44:16 +08:00
parent 7c6275c03a
commit 97f6e8c8e2
4 changed files with 388 additions and 5 deletions

View 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;

View File

@ -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 />,

View 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();

View File

@ -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}`;