2026-04-05 12:43:24 +00:00
|
|
|
import React, { useState, useEffect } from 'react';
|
2026-04-05 14:21:47 +00:00
|
|
|
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';
|
2026-04-05 12:43:24 +00:00
|
|
|
import api from '../api';
|
|
|
|
|
|
2026-04-05 14:21:47 +00:00
|
|
|
const STATUS_CONFIG = {
|
|
|
|
|
consistent: {
|
|
|
|
|
color: 'success',
|
|
|
|
|
icon: <CheckCircleOutlined />,
|
|
|
|
|
text: '内容一致',
|
|
|
|
|
description: '本地文件与数据库内容完全相同',
|
|
|
|
|
},
|
|
|
|
|
local_newer: {
|
|
|
|
|
color: 'warning',
|
|
|
|
|
icon: <SyncOutlined spin />,
|
|
|
|
|
text: '本地更新',
|
|
|
|
|
description: '本地文件比数据库更新',
|
|
|
|
|
},
|
|
|
|
|
db_newer: {
|
|
|
|
|
color: 'info',
|
|
|
|
|
icon: <SyncOutlined spin />,
|
|
|
|
|
text: '数据库更新',
|
|
|
|
|
description: '数据库文件比本地更新',
|
|
|
|
|
},
|
|
|
|
|
conflict: {
|
|
|
|
|
color: 'error',
|
|
|
|
|
icon: <ExclamationCircleOutlined />,
|
|
|
|
|
text: '存在冲突',
|
|
|
|
|
description: '本地与数据库内容不一致',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-05 12:43:24 +00:00
|
|
|
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/', {
|
2026-04-05 14:21:47 +00:00
|
|
|
params: {
|
|
|
|
|
lobster_id,
|
|
|
|
|
file_path: filePath,
|
|
|
|
|
chunked: 'true',
|
|
|
|
|
},
|
2026-04-05 12:43:24 +00:00
|
|
|
});
|
|
|
|
|
|
2026-04-05 14:21:47 +00:00
|
|
|
if (response.data.success) {
|
|
|
|
|
setDiffData(response.data.data);
|
2026-04-05 12:43:24 +00:00
|
|
|
} else {
|
2026-04-05 14:21:47 +00:00
|
|
|
setError(response.data.error || '加载失败');
|
2026-04-05 12:43:24 +00:00
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err.message || '网络错误');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (filePath) {
|
|
|
|
|
loadDiff();
|
|
|
|
|
}
|
2026-04-05 14:21:47 +00:00
|
|
|
}, [filePath, lobsterId]);
|
2026-04-05 12:43:24 +00:00
|
|
|
|
2026-04-05 14:21:47 +00:00
|
|
|
const renderDiff = () => {
|
|
|
|
|
if (!diffData) return null;
|
2026-04-05 12:43:24 +00:00
|
|
|
|
2026-04-05 14:21:47 +00:00
|
|
|
const { local_content, db_content, diff, status } = diffData;
|
2026-04-05 12:43:24 +00:00
|
|
|
|
2026-04-05 14:21:47 +00:00
|
|
|
if (!local_content || !db_content) {
|
|
|
|
|
return (
|
|
|
|
|
<Alert
|
|
|
|
|
message="文件不存在"
|
|
|
|
|
description={local_content ? '数据库中不存在此文件' : '本地不存在此文件'}
|
|
|
|
|
type="info"
|
|
|
|
|
showIcon
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 使用 diff 库计算行级差异
|
|
|
|
|
const changes = diffLines(db_content || '', local_content || '');
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ maxHeight: '600px', overflowY: 'auto' }}>
|
|
|
|
|
<Descriptions
|
|
|
|
|
bordered
|
|
|
|
|
size="small"
|
|
|
|
|
column={2}
|
|
|
|
|
style={{ marginBottom: 16 }}
|
|
|
|
|
>
|
|
|
|
|
<Descriptions.Item label="状态" span={2}>
|
|
|
|
|
<Badge
|
|
|
|
|
status={STATUS_CONFIG[status]?.color}
|
|
|
|
|
text={
|
|
|
|
|
<Space>
|
|
|
|
|
{STATUS_CONFIG[status]?.icon}
|
|
|
|
|
{STATUS_CONFIG[status]?.text}
|
|
|
|
|
</Space>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
{diff.lines_changed !== 0 && (
|
|
|
|
|
<Descriptions.Item label="变动行数" span={2}>
|
|
|
|
|
<Tag color={diff.lines_changed > 0 ? 'green' : 'red'}>
|
|
|
|
|
{diff.lines_changed > 0 ? '+' : ''}{diff.lines_changed}
|
|
|
|
|
</Tag>
|
|
|
|
|
{diff.is_truncated && (
|
|
|
|
|
<Tooltip title="大文件,仅显示头尾差异">
|
|
|
|
|
<Tag color="orange">已截断</Tag>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
)}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
)}
|
|
|
|
|
<Descriptions.Item label="本地哈希" span={1}>
|
|
|
|
|
<code style={{ fontSize: '12px' }}>
|
|
|
|
|
{diffData.local_hash?.slice(0, 16)}...
|
|
|
|
|
</code>
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="数据库哈希" span={1}>
|
|
|
|
|
<code style={{ fontSize: '12px' }}>
|
|
|
|
|
{diffData.db_hash?.slice(0, 16)}...
|
|
|
|
|
</code>
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
</Descriptions>
|
2026-04-05 12:43:24 +00:00
|
|
|
|
2026-04-05 14:21:47 +00:00
|
|
|
<div className="diff-container">
|
|
|
|
|
{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 (
|
|
|
|
|
<div
|
|
|
|
|
key={index}
|
|
|
|
|
style={{
|
|
|
|
|
...lineStyle,
|
|
|
|
|
backgroundColor: '#e6fffb',
|
|
|
|
|
borderLeft: '3px solid #52c41a',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span style={{ color: '#52c41a', marginRight: '8px' }}>+</span>
|
|
|
|
|
{change.value}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
} else if (change.type === ChangeType.Delete) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={index}
|
|
|
|
|
style={{
|
|
|
|
|
...lineStyle,
|
|
|
|
|
backgroundColor: '#fff1f0',
|
|
|
|
|
borderLeft: '3px solid #ff4d4f',
|
|
|
|
|
textDecoration: 'line-through',
|
|
|
|
|
opacity: 0.7,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span style={{ color: '#ff4d4f', marginRight: '8px' }}>-</span>
|
|
|
|
|
{change.value}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
return (
|
|
|
|
|
<div key={index} style={{ ...lineStyle }}>
|
|
|
|
|
<span style={{ color: '#d9d9d9', marginRight: '8px' }}> </span>
|
|
|
|
|
{change.value}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
2026-04-05 12:43:24 +00:00
|
|
|
|
2026-04-05 14:21:47 +00:00
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '60px 0' }}>
|
|
|
|
|
<Spin size="large" tip="加载中..." />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-04-05 12:43:24 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 14:21:47 +00:00
|
|
|
if (error) {
|
2026-04-05 12:43:24 +00:00
|
|
|
return (
|
|
|
|
|
<Alert
|
2026-04-05 14:21:47 +00:00
|
|
|
message="加载失败"
|
|
|
|
|
description={error}
|
|
|
|
|
type="error"
|
2026-04-05 12:43:24 +00:00
|
|
|
showIcon
|
2026-04-05 14:21:47 +00:00
|
|
|
action={
|
|
|
|
|
<Button size="small" onClick={loadDiff}>
|
|
|
|
|
重试
|
|
|
|
|
</Button>
|
|
|
|
|
}
|
2026-04-05 12:43:24 +00:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 14:21:47 +00:00
|
|
|
if (!diffData) {
|
2026-04-05 12:43:24 +00:00
|
|
|
return (
|
|
|
|
|
<Alert
|
2026-04-05 14:21:47 +00:00
|
|
|
message="请选择文件"
|
|
|
|
|
description="点击左侧文件树中的文件查看差异"
|
|
|
|
|
type="info"
|
2026-04-05 12:43:24 +00:00
|
|
|
showIcon
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-04-05 14:21:47 +00:00
|
|
|
<div className="file-diff">
|
|
|
|
|
<div style={{ marginBottom: 16 }}>
|
|
|
|
|
<Space>
|
|
|
|
|
<Button
|
|
|
|
|
size="small"
|
|
|
|
|
icon={<ClockCircleOutlined />}
|
|
|
|
|
onClick={loadDiff}
|
|
|
|
|
>
|
|
|
|
|
刷新
|
|
|
|
|
</Button>
|
|
|
|
|
</Space>
|
|
|
|
|
</div>
|
2026-04-05 12:43:24 +00:00
|
|
|
|
2026-04-05 14:21:47 +00:00
|
|
|
{STATUS_CONFIG[diffData.status] && (
|
|
|
|
|
<Alert
|
|
|
|
|
message={STATUS_CONFIG[diffData.status].text}
|
|
|
|
|
description={STATUS_CONFIG[diffData.status].description}
|
|
|
|
|
type={STATUS_CONFIG[diffData.status].color}
|
|
|
|
|
showIcon
|
|
|
|
|
icon={STATUS_CONFIG[diffData.status].icon}
|
|
|
|
|
style={{ marginBottom: 16 }}
|
|
|
|
|
closable
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{renderDiff()}
|
2026-04-05 12:43:24 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|