From 6d87b7cecccc11818b52c86c28fdd14ef22c6bc5 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Sun, 10 Aug 2025 07:48:20 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=A1=B5=E9=9D=A2=E6=AF=94?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/DiffViewer.tsx | 231 ++++++++++++++++++++++++++++ src/components/GitPanelEnhanced.tsx | 28 +++- 2 files changed, 255 insertions(+), 4 deletions(-) create mode 100644 src/components/DiffViewer.tsx diff --git a/src/components/DiffViewer.tsx b/src/components/DiffViewer.tsx new file mode 100644 index 0000000..f7434e1 --- /dev/null +++ b/src/components/DiffViewer.tsx @@ -0,0 +1,231 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { invoke } from "@tauri-apps/api/core"; +import { + X, + FileDiff, + AlertCircle, + Loader2 +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { useTranslation } from "react-i18next"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Badge } from "@/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +interface DiffViewerProps { + projectPath: string; + filePath: string; + staged?: boolean; + isVisible: boolean; + onClose: () => void; + className?: string; +} + +export const DiffViewer: React.FC = ({ + projectPath, + filePath, + staged = false, + isVisible, + onClose, + className, +}) => { + const { t } = useTranslation(); + const [diffContent, setDiffContent] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [diffStats, setDiffStats] = useState<{ additions: number; deletions: number }>({ additions: 0, deletions: 0 }); + + const fileName = filePath.split("/").pop() || filePath; + + // 加载差异内容 + const loadDiff = useCallback(async () => { + if (!filePath || !projectPath) return; + + try { + setLoading(true); + setError(null); + + const diff = await invoke("get_git_diff", { + path: projectPath, + filePath: filePath, + staged: staged + }); + + setDiffContent(diff || "No changes"); + + // 计算差异统计 + const lines = diff.split('\n'); + let additions = 0; + let deletions = 0; + lines.forEach(line => { + if (line.startsWith('+') && !line.startsWith('+++')) additions++; + if (line.startsWith('-') && !line.startsWith('---')) deletions++; + }); + setDiffStats({ additions, deletions }); + } catch (err) { + console.error("Failed to load diff:", err); + setError(err instanceof Error ? err.message : "Failed to load diff"); + } finally { + setLoading(false); + } + }, [filePath, projectPath, staged]); + + // 处理关闭 + const handleClose = useCallback(() => { + onClose(); + }, [onClose]); + + // 键盘快捷键 + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Esc 关闭 + if (e.key === "Escape") { + handleClose(); + } + }; + + if (isVisible) { + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + } + }, [isVisible, handleClose]); + + // 加载差异 + useEffect(() => { + if (isVisible && filePath && projectPath) { + loadDiff(); + } + }, [isVisible, filePath, projectPath, loadDiff]); + + return ( + + {isVisible && ( + + e.stopPropagation()} + > + {/* Header */} +
+
+ +
+

{fileName}

+

{filePath}

+
+ + {staged ? "Staged" : "Modified"} + + {(diffStats.additions > 0 || diffStats.deletions > 0) && ( + <> +
+
+ + +{diffStats.additions} + + + -{diffStats.deletions} + +
+ + )} +
+ +
+ + + + + + +

{t("app.close")}

+
+
+
+
+
+ + {/* Content */} + {error ? ( +
+ +

{t("app.error")}

+

{error}

+
+ ) : loading ? ( +
+ +
+ ) : ( + +
+
+
+                      {diffContent.split('\n').map((line, index) => {
+                        let className = "block px-3 py-0.5 -mx-3 ";
+                        let lineContent = line;
+                        
+                        if (line.startsWith('+') && !line.startsWith('+++')) {
+                          className += "bg-green-500/10 text-green-600 dark:text-green-400 border-l-4 border-green-500";
+                          lineContent = line.substring(1);
+                        } else if (line.startsWith('-') && !line.startsWith('---')) {
+                          className += "bg-red-500/10 text-red-600 dark:text-red-400 border-l-4 border-red-500";
+                          lineContent = line.substring(1);
+                        } else if (line.startsWith('@@')) {
+                          className += "bg-blue-500/10 text-blue-600 dark:text-blue-400 font-semibold my-2 py-1 rounded";
+                        } else if (line.startsWith('diff --git')) {
+                          className += "text-primary font-bold mt-6 mb-2 pt-4 border-t-2 border-border";
+                          if (index > 0) className += " mt-8";
+                        } else if (line.startsWith('index ') || line.startsWith('+++') || line.startsWith('---')) {
+                          className += "text-muted-foreground text-xs italic opacity-70";
+                        } else {
+                          className += "text-foreground/80 hover:bg-muted/30 transition-colors";
+                          // 移除行首空格(如果存在)
+                          if (line.startsWith(' ')) {
+                            lineContent = line.substring(1);
+                          }
+                        }
+                        
+                        return (
+                          
+                            {lineContent || ' '}
+                          
+                        );
+                      })}
+                    
+
+
+
+ )} + + + )} + + ); +}; + +export default DiffViewer; \ No newline at end of file diff --git a/src/components/GitPanelEnhanced.tsx b/src/components/GitPanelEnhanced.tsx index c686492..c0e9303 100644 --- a/src/components/GitPanelEnhanced.tsx +++ b/src/components/GitPanelEnhanced.tsx @@ -40,6 +40,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { useTranslation } from "@/hooks/useTranslation"; +import DiffViewer from "./DiffViewer"; interface GitFileStatus { @@ -142,6 +143,9 @@ export const GitPanelEnhanced: React.FC = ({ const [isResizing, setIsResizing] = useState(false); const [expandedNodes, setExpandedNodes] = useState>(new Set()); const [selectedPath, setSelectedPath] = useState(null); + const [showDiffViewer, setShowDiffViewer] = useState(false); + const [diffFilePath, setDiffFilePath] = useState(""); + const [diffStaged, setDiffStaged] = useState(false); const panelRef = useRef(null); const resizeHandleRef = useRef(null); @@ -247,8 +251,13 @@ export const GitPanelEnhanced: React.FC = ({ }; }, [isVisible, projectPath, activeTab, loadGitStatus, loadCommits]); - // 处理文件点击 - const handleFileClick = (filePath: string) => { + // 处理文件点击 - 打开 DiffViewer + const handleFileClick = (filePath: string, staged: boolean = false) => { + setDiffFilePath(filePath); + setDiffStaged(staged); + setShowDiffViewer(true); + + // 如果有文件选择回调,也调用它 if (onFileSelect) { const fullPath = `${projectPath}/${filePath}`; onFileSelect(fullPath); @@ -473,7 +482,7 @@ export const GitPanelEnhanced: React.FC = ({ if (isDirectory) { toggleExpand(node.path); } else { - handleFileClick(node.path); + handleFileClick(node.path, statusType === 'staged'); } }} > @@ -614,7 +623,8 @@ export const GitPanelEnhanced: React.FC = ({ } : null; return ( - + <> + {isVisible && ( = ({ )} + + {/* Diff Viewer Modal */} + setShowDiffViewer(false)} + /> + ); };