import React, { useState, useEffect } from 'react'; import { Spin, Alert, Tag, Button, Descriptions, Space, Tooltip, Badge } from 'antd'; import { CheckCircleOutlined, ExclamationCircleOutlined, SyncOutlined, ClockCircleOutlined, FileTextOutlined, } from '@ant-design/icons'; import { diffLines, ChangeType } from 'diff'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import api from '../api'; const STATUS_CONFIG = { consistent: { color: 'success', icon: , text: '内容一致', description: '本地文件与数据库内容完全相同', }, local_newer: { color: 'warning', icon: , text: '本地更新', description: '本地文件比数据库更新', }, db_newer: { color: 'info', icon: , text: '数据库更新', description: '数据库文件比本地更新', }, conflict: { color: 'error', icon: , text: '存在冲突', description: '本地与数据库内容不一致', }, }; export default function FileDiff({ filePath, lobsterId }) { const [loading, setLoading] = useState(false); const [diffData, setDiffData] = useState(null); const [error, setError] = useState(null); const loadDiff = async () => { setLoading(true); setError(null); try { const response = await api.get('/diff/', { params: { lobster_id, file_path: filePath, chunked: 'true', }, }); if (response.data.success) { setDiffData(response.data.data); } else { setError(response.data.error || '加载失败'); } } catch (err) { setError(err.message || '网络错误'); } finally { setLoading(false); } }; useEffect(() => { if (filePath) { loadDiff(); } }, [filePath, lobsterId]); const renderDiff = () => { if (!diffData) return null; const { local_content, db_content, diff, status } = diffData; if (!local_content || !db_content) { return ( ); } // 使用 diff 库计算行级差异 const changes = diffLines(db_content || '', local_content || ''); return (
{STATUS_CONFIG[status]?.icon} {STATUS_CONFIG[status]?.text} } /> {diff.lines_changed !== 0 && ( 0 ? 'green' : 'red'}> {diff.lines_changed > 0 ? '+' : ''}{diff.lines_changed} {diff.is_truncated && ( 已截断 )} )} {diffData.local_hash?.slice(0, 16)}... {diffData.db_hash?.slice(0, 16)}...
{changes.map((change, index) => { const lineStyle = { paddingLeft: '16px', paddingRight: '16px', margin: '2px 0', fontSize: '13px', fontFamily: 'Consolas, Monaco, "Courier New", monospace', lineHeight: '1.6', }; if (change.type === ChangeType.Insert) { return (
+ {change.value}
); } else if (change.type === ChangeType.Delete) { return (
- {change.value}
); } else { return (
{change.value}
); } })}
); }; if (loading) { return (
); } if (error) { return ( 重试 } /> ); } if (!diffData) { return ( ); } return (
{STATUS_CONFIG[diffData.status] && ( )} {renderDiff()}
); }