feat(ui): 添加底部状态栏和优化代码编辑器空状态

- 新增 StatusBar 组件,显示 Git 分支、诊断信息和连接状态
- 添加 Git API 端点 (GET /api/files/git) 获取分支和 dirty 状态
- 优化 CodeEditor 空状态,添加图标和引导提示
- 修复 Chat 页面高度问题 (h-screen -> h-full)
This commit is contained in:
2025-12-17 17:04:38 +08:00
parent 250d2cb4b5
commit a3ddc39771
7 changed files with 215 additions and 6 deletions
+20 -3
View File
@@ -13,7 +13,7 @@ import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { markdown } from '@codemirror/lang-markdown';
import { oneDark } from '@codemirror/theme-one-dark';
import { X, Save, Circle } from 'lucide-react';
import { X, Save, Circle, FileCode, MousePointerClick } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { toast } from 'sonner';
import { cn } from '../utils/cn.js';
@@ -156,8 +156,25 @@ export function CodeEditor({
if (tabs.length === 0) {
return (
<div className={cn('flex items-center justify-center h-full bg-surface-base text-fg-muted', className)}>
<p>No files open</p>
<div className={cn('flex flex-col items-center justify-center h-full bg-surface-base', className)}>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="text-center max-w-xs"
>
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-surface-subtle flex items-center justify-center">
<FileCode size={32} className="text-fg-subtle" />
</div>
<h3 className="text-fg font-medium mb-2">No files open</h3>
<p className="text-fg-muted text-sm mb-4">
Select a file from the explorer to view and edit its contents
</p>
<div className="flex items-center justify-center gap-2 text-xs text-fg-subtle">
<MousePointerClick size={14} />
<span>Click a file to open</span>
</div>
</motion.div>
</div>
);
}
+140
View File
@@ -0,0 +1,140 @@
/**
* StatusBar Component
*
* 底部状态栏,显示 Git 分支、诊断信息、连接状态等
*/
import { useState, useEffect, useCallback } from 'react';
import { GitBranch, AlertTriangle, AlertCircle, Wifi, WifiOff, RefreshCw, CheckCircle } from 'lucide-react';
import { cn } from '../utils/cn.js';
import { getLSPDiagnostics, getGitInfo, type DiagnosticsSummary, type GitInfo } from '../api/client.js';
interface StatusBarProps {
className?: string;
/** 是否连接到服务器 */
isConnected?: boolean;
/** 点击诊断信息回调 */
onDiagnosticsClick?: () => void;
/** 刷新间隔 (ms) */
refreshInterval?: number;
}
export function StatusBar({
className,
isConnected = true,
onDiagnosticsClick,
refreshInterval = 30000,
}: StatusBarProps) {
const [diagnostics, setDiagnostics] = useState<DiagnosticsSummary | null>(null);
const [gitInfo, setGitInfo] = useState<GitInfo | null>(null);
const [loading, setLoading] = useState(false);
// 加载诊断信息和 Git 信息
const loadData = useCallback(async () => {
setLoading(true);
try {
const [diagResult, gitResult] = await Promise.all([
getLSPDiagnostics(),
getGitInfo(),
]);
if (diagResult.success && diagResult.data.summary) {
setDiagnostics(diagResult.data.summary);
}
if (gitResult.success && gitResult.data) {
setGitInfo(gitResult.data);
}
} catch {
// 忽略错误
} finally {
setLoading(false);
}
}, []);
// 初始加载和定时刷新
useEffect(() => {
loadData();
const interval = setInterval(loadData, refreshInterval);
return () => clearInterval(interval);
}, [loadData, refreshInterval]);
const errorCount = diagnostics?.totalErrors ?? 0;
const warningCount = diagnostics?.totalWarnings ?? 0;
const hasIssues = errorCount > 0 || warningCount > 0;
return (
<div
className={cn(
'flex items-center justify-between h-6 px-2 text-xs',
'bg-surface-emphasis border-t border-line',
'text-fg-muted select-none',
className
)}
>
{/* 左侧 */}
<div className="flex items-center gap-3">
{/* Git 分支 */}
{gitInfo && (
<div className="flex items-center gap-1 hover:text-fg-secondary cursor-default">
<GitBranch size={12} />
<span>{gitInfo.branch}</span>
{gitInfo.dirty && <span className="text-orange-400">*</span>}
</div>
)}
{/* 刷新按钮 */}
<button
onClick={loadData}
disabled={loading}
className="p-0.5 hover:text-fg-secondary transition-colors"
title="Refresh"
>
<RefreshCw size={12} className={cn(loading && 'animate-spin')} />
</button>
</div>
{/* 右侧 */}
<div className="flex items-center gap-3">
{/* 诊断信息 */}
<button
onClick={onDiagnosticsClick}
className={cn(
'flex items-center gap-2 hover:text-fg-secondary transition-colors',
hasIssues && 'text-fg-secondary'
)}
title="View diagnostics"
>
{errorCount > 0 && (
<span className="flex items-center gap-0.5 text-red-400">
<AlertCircle size={12} />
<span>{errorCount}</span>
</span>
)}
{warningCount > 0 && (
<span className="flex items-center gap-0.5 text-yellow-400">
<AlertTriangle size={12} />
<span>{warningCount}</span>
</span>
)}
{!hasIssues && (
<span className="flex items-center gap-0.5 text-green-400">
<CheckCircle size={12} />
</span>
)}
</button>
{/* 连接状态 */}
<div
className={cn(
'flex items-center gap-1',
isConnected ? 'text-green-400' : 'text-red-400'
)}
title={isConnected ? 'Connected to server' : 'Disconnected from server'}
>
{isConnected ? <Wifi size={12} /> : <WifiOff size={12} />}
</div>
</div>
</div>
);
}