|
|
@@ -89,23 +89,24 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
const [error, setError] = useState('');
|
|
|
const [updateTrigger, setUpdateTrigger] = useState(0);
|
|
|
- // 新增:客户端挂载标志
|
|
|
+ // 客户端挂载标志 - 用于避免 hydration 错误
|
|
|
const [mounted, setMounted] = useState(false);
|
|
|
- // 新增:健康检查状态
|
|
|
- const [isHealthy, setIsHealthy] = useState<boolean | null>(null); // null=未检查, true=健康, false=不健康
|
|
|
+ // 启用/禁用状态 - 初始值统一为 true,客户端挂载后从 localStorage 读取真实值
|
|
|
+ const [isEnabled, setIsEnabled] = useState(true);
|
|
|
+ // 健康检查状态
|
|
|
+ const [isHealthy, setIsHealthy] = useState<boolean | null>(null);
|
|
|
const [isCheckingHealth, setIsCheckingHealth] = useState(false);
|
|
|
const [latency, setLatency] = useState<number | null>(null);
|
|
|
- // 新增:启用/禁用状态 - 初始值设为 true 避免闪烁,useEffect 中会更新真实值
|
|
|
- const [isEnabled, setIsEnabled] = useState(true);
|
|
|
|
|
|
// 监听 localStorage 变化和组件挂载
|
|
|
useEffect(() => {
|
|
|
// 设置客户端挂载标志
|
|
|
setMounted(true);
|
|
|
|
|
|
- // 初始化启用状态 - 从 localStorage 读取真实值
|
|
|
- const realEnabledState = mcpTokenManager.isEnabled(mcpType);
|
|
|
- setIsEnabled(realEnabledState);
|
|
|
+ // 从 localStorage 读取真实的启用状态
|
|
|
+ const storedEnabled = mcpTokenManager.isEnabled(mcpType);
|
|
|
+ console.log(`[McpServerCard ${mcpType}] Client mounted, isEnabled from localStorage: ${storedEnabled}`);
|
|
|
+ setIsEnabled(storedEnabled);
|
|
|
|
|
|
// 初始化时检查登录状态
|
|
|
const checkLoginStatus = () => {
|
|
|
@@ -191,11 +192,16 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
|
|
|
}, [isHealthy, isLoggedIn, mcpType, onConnectionStatusChange]);
|
|
|
|
|
|
// 计算连接状态显示
|
|
|
+ // - 未挂载 → 加载中
|
|
|
// - 禁用 → 灰色圆点
|
|
|
// - 健康 + 已登录 → 已连接 ✓ | 已登录 ✓
|
|
|
// - 健康 + 未登录 → 已连接 ✓ | 未登录
|
|
|
// - 不健康 → 离线
|
|
|
const getConnectionStatus = () => {
|
|
|
+ // 未挂载时显示加载状态,避免 hydration 错误
|
|
|
+ if (!mounted) {
|
|
|
+ return { text: '加载中...', className: 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400', dotColor: 'bg-gray-300' };
|
|
|
+ }
|
|
|
// 禁用状态优先,显示灰色
|
|
|
if (!isEnabled) {
|
|
|
return { text: '已禁用', className: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400', dotColor: 'bg-gray-400' };
|
|
|
@@ -255,14 +261,19 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
|
|
|
};
|
|
|
|
|
|
const handleToggleEnabled = () => {
|
|
|
+ console.log(`[McpServerCard.handleToggleEnabled] Called for ${mcpType}, current isEnabled=${isEnabled}`);
|
|
|
const newState = mcpTokenManager.toggleEnabled(mcpType);
|
|
|
+ console.log(`[McpServerCard.handleToggleEnabled] toggleEnabled returned: ${newState}`);
|
|
|
setIsEnabled(newState);
|
|
|
onEnabledChange?.(mcpType, newState);
|
|
|
+ // 触发自定义事件,通知 Header 等组件更新状态
|
|
|
+ window.dispatchEvent(new CustomEvent('mcp-enabled-change'));
|
|
|
+ console.log(`[McpServerCard.handleToggleEnabled] Dispatched mcp-enabled-change event`);
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
<div
|
|
|
- className={`bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 md:p-6 border dark:border-gray-700 transition-all ${!isEnabled ? 'opacity-50 grayscale' : ''}`}
|
|
|
+ className={`bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 md:p-6 border dark:border-gray-700 transition-all ${mounted && !isEnabled ? 'opacity-50 grayscale' : ''}`}
|
|
|
suppressHydrationWarning
|
|
|
>
|
|
|
{/* 头部区域 - 移动端优化 */}
|
|
|
@@ -275,26 +286,27 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
|
|
|
</h3>
|
|
|
<p className="text-xs md:text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
|
|
{config.authType === 'none' ? '无需认证' : '需要登录'}
|
|
|
- {latency !== null && isHealthy && isEnabled && (
|
|
|
+ {mounted && latency !== null && isHealthy && isEnabled && (
|
|
|
<span className="ml-2 text-xs text-gray-400">
|
|
|
({latency}ms)
|
|
|
</span>
|
|
|
)}
|
|
|
</p>
|
|
|
</div>
|
|
|
- {/* 启用/禁用按钮 - 移动端加大点击区域 */}
|
|
|
+ {/* 启用/禁用按钮 - 移动端加大点击区域,只在客户端显示真实状态 */}
|
|
|
<button
|
|
|
type="button"
|
|
|
onClick={handleToggleEnabled}
|
|
|
className={`flex items-center gap-1.5 px-3 py-2 md:px-2.5 md:py-1 rounded-full text-xs md:text-sm font-medium transition-all min-h-[44px] md:min-h-0 ${
|
|
|
+ !mounted ? 'bg-gray-100 dark:bg-gray-700 text-gray-400' :
|
|
|
isEnabled
|
|
|
? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 hover:bg-green-200 dark:hover:bg-green-800'
|
|
|
: 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
|
|
|
}`}
|
|
|
- title={isEnabled ? '点击禁用此 MCP 服务器' : '点击启用此 MCP 服务器'}
|
|
|
+ title={mounted ? (isEnabled ? '点击禁用此 MCP 服务器' : '点击启用此 MCP 服务器') : '加载中...'}
|
|
|
>
|
|
|
- <span className={`w-2 h-2 rounded-full ${isEnabled ? 'bg-green-500' : 'bg-gray-400'}`} />
|
|
|
- {isEnabled ? '禁用' : '启用'}
|
|
|
+ <span className={`w-2 h-2 rounded-full ${!mounted ? 'bg-gray-300' : isEnabled ? 'bg-green-500' : 'bg-gray-400'}`} />
|
|
|
+ {!mounted ? '...' : isEnabled ? '禁用' : '启用'}
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
@@ -314,8 +326,8 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
|
|
|
)}
|
|
|
</div>
|
|
|
|
|
|
- {/* 已登录状态显示 - 移动端优化 */}
|
|
|
- {isLoggedIn && (
|
|
|
+ {/* 已登录状态显示 - 移动端优化,只在客户端渲染 */}
|
|
|
+ {mounted && isLoggedIn && (
|
|
|
<div className="mb-4 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
|
|
<p className="text-sm text-green-800 dark:text-green-200">
|
|
|
已登录: <span className="font-medium">{username}</span>
|
|
|
@@ -326,8 +338,8 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
- {/* 禁用提示 - 移动端优化 */}
|
|
|
- {!isEnabled && (
|
|
|
+ {/* 禁用提示 - 移动端优化,只在客户端渲染 */}
|
|
|
+ {mounted && !isEnabled && (
|
|
|
<div className="mb-4 p-3 bg-gray-100 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600">
|
|
|
<p className="text-xs md:text-sm text-gray-600 dark:text-gray-400">
|
|
|
此 MCP 服务器已禁用。点击右上角的"启用"按钮来启用它。
|
|
|
@@ -335,7 +347,7 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
- {showLoginForm && !isLoggedIn && isEnabled && (
|
|
|
+ {mounted && showLoginForm && !isLoggedIn && isEnabled && (
|
|
|
<form onSubmit={handleLogin} className="mb-4 space-y-3">
|
|
|
<div>
|
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
|
@@ -395,7 +407,7 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
|
|
|
</form>
|
|
|
)}
|
|
|
|
|
|
- {showTokenForm && !isLoggedIn && (
|
|
|
+ {mounted && showTokenForm && !isLoggedIn && (
|
|
|
<div className="mb-4 space-y-3">
|
|
|
<div>
|
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
|
@@ -453,7 +465,7 @@ export function McpServerCard({ mcpType, config, onConnectionStatusChange, onEna
|
|
|
{config.url}
|
|
|
</p>
|
|
|
{/* 操作按钮 - 移动端加大点击区域,按钮靠左对齐 */}
|
|
|
- {config.authType === 'jwt' && isEnabled && (
|
|
|
+ {mounted && config.authType === 'jwt' && isEnabled && (
|
|
|
<div className="flex gap-3 md:gap-2">
|
|
|
{!isLoggedIn ? (
|
|
|
!showLoginForm && !showTokenForm && (
|