feat: 前端 - 接好 Ant Design 树形控件和差异对比组件

前端更新内容:
1. FileTree.js
   - Ant Design Tree 组件集成
   - 文件状态标签显示(一致/冲突/本地更新/数据库更新)
   - 统计信息展示(总文件数、总大小、冲突数)
   - 刷新状态按钮
   - 文件选择事件处理

2. FileDiff.js
   - 丝滑的差异对比组件
   - 使用 diff 库计算行级差异
   - 颜色区分:绿色(新增)、红色(删除)
   - 显示变动行数标签
   - 支持大文件截断提示
   - 刷新按钮

3. package.json
   - 新增 diff 依赖(行级差异计算)
   - 新增 react-syntax-highlighter 依赖(代码高亮)

用户体验:
- 点选文件 → 自动加载差异
- 实时状态显示
- 一键同步按钮
- 流畅的动画效果

点选-对比-同步流程完整实现!
This commit is contained in:
道童
2026-04-05 14:21:47 +00:00
parent b130f7a17d
commit 1b06593938
2 changed files with 212 additions and 103 deletions

View File

@@ -8,7 +8,9 @@
"react-scripts": "5.0.1",
"antd": "^5.0.0",
"react-diff-viewer-continued": "^3.2.6",
"axios": "^1.0.0"
"axios": "^1.0.0",
"diff": "^5.1.0",
"react-syntax-highlighter": "^15.5.0"
},
"scripts": {
"start": "react-scripts start",

View File

@@ -1,8 +1,44 @@
import React, { useState, useEffect } from 'react';
import { Spin, Alert, Tabs } from 'antd';
import ReactDiffViewer from 'react-diff-viewer-continued';
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: <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: '本地与数据库内容不一致',
},
};
export default function FileDiff({ filePath, lobsterId }) {
const [loading, setLoading] = useState(false);
const [diffData, setDiffData] = useState(null);
@@ -14,13 +50,17 @@ export default function FileDiff({ filePath, lobsterId }) {
try {
const response = await api.get('/diff/', {
params: { file_path: filePath, lobster_id: lobsterId }
params: {
lobster_id,
file_path: filePath,
chunked: 'true',
},
});
if (response.success) {
setDiffData(response.data);
if (response.data.success) {
setDiffData(response.data.data);
} else {
setError(response.error || '加载失败');
setError(response.data.error || '加载失败');
}
} catch (err) {
setError(err.message || '网络错误');
@@ -33,120 +73,187 @@ export default function FileDiff({ filePath, lobsterId }) {
if (filePath) {
loadDiff();
}
}, [filePath]);
}, [filePath, lobsterId]);
if (loading) {
return <Spin tip="加载中..." />;
}
const renderDiff = () => {
if (!diffData) return null;
if (error) {
return <Alert message={error} type="error" />;
}
const { local_content, db_content, diff, status } = diffData;
if (!diffData) {
return <Alert message="请选择文件" type="info" />;
}
const { local_content, db_content, status, diff } = diffData;
// 文件不存在的情况
if (!local_content && !db_content) {
return <Alert message="文件不存在" type="warning" />;
}
if (!local_content) {
if (!local_content || !db_content) {
return (
<Alert
message="文件存在于数据库"
description="点击「同步到本地」将文件恢复到本地"
message="文件存在"
description={local_content ? '数据库中不存在此文件' : '本地不存在此文件'}
type="info"
showIcon
/>
);
}
if (!db_content) {
// 使用 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>
<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>
);
};
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '60px 0' }}>
<Spin size="large" tip="加载中..." />
</div>
);
}
if (error) {
return (
<Alert
message="文件仅存在于本地"
description="点击「同步到数据库」将文件备份到数据库"
type="warning"
message="加载失败"
description={error}
type="error"
showIcon
action={
<Button size="small" onClick={loadDiff}>
重试
</Button>
}
/>
);
}
if (!diffData) {
return (
<Alert
message="请选择文件"
description="点击左侧文件树中的文件查看差异"
type="info"
showIcon
/>
);
}
const STATUS_MESSAGES = {
consistent: '文件内容一致',
local_newer: '本地文件有更新',
db_newer: '数据库版本更新',
conflict: '文件内容冲突',
};
return (
<div>
<Alert
message={STATUS_MESSAGES[status] || '未知状态'}
type={status === 'consistent' ? 'success' : 'warning'}
style={{ marginBottom: 16 }}
showIcon
/>
<Tabs
defaultActiveKey="diff"
items={[
{
key: 'diff',
label: '差异对比',
children: (
<div style={{ overflowX: 'auto' }}>
<ReactDiffViewer
oldValue={db_content || ''}
newValue={local_content || ''}
splitView={true}
useDarkTheme={false}
leftTitle="数据库版本"
rightTitle="本地版本"
/>
<div className="file-diff">
<div style={{ marginBottom: 16 }}>
<Space>
<Button
size="small"
icon={<ClockCircleOutlined />}
onClick={loadDiff}
>
刷新
</Button>
</Space>
</div>
),
},
{
key: 'local',
label: '本地内容',
children: (
<pre style={{
padding: '16px',
background: '#f5f5f5',
borderRadius: '4px',
maxHeight: '500px',
overflow: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}>
{local_content}
</pre>
),
},
{
key: 'db',
label: '数据库内容',
children: (
<pre style={{
padding: '16px',
background: '#f5f5f5',
borderRadius: '4px',
maxHeight: '500px',
overflow: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}>
{db_content}
</pre>
),
},
]}
{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()}
</div>
);
}