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:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user