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 (
}
onClick={loadDiff}
>
刷新
{STATUS_CONFIG[diffData.status] && (
)}
{renderDiff()}
);
}