diff --git a/frontend/package.json b/frontend/package.json
index 1856160..74520e9 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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",
diff --git a/frontend/src/components/FileDiff.js b/frontend/src/components/FileDiff.js
index c681a40..ee1aa7a 100644
--- a/frontend/src/components/FileDiff.js
+++ b/frontend/src/components/FileDiff.js
@@ -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: ,
+ 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);
@@ -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]);
+
+ 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 ;
+ return (
+
+
+
+ );
}
if (error) {
- return ;
+ return (
+
+ 重试
+
+ }
+ />
+ );
}
if (!diffData) {
- return ;
- }
-
- const { local_content, db_content, status, diff } = diffData;
-
- // 文件不存在的情况
- if (!local_content && !db_content) {
- return ;
- }
-
- if (!local_content) {
return (
);
}
- if (!db_content) {
- return (
-
- );
- }
-
- const STATUS_MESSAGES = {
- consistent: '文件内容一致',
- local_newer: '本地文件有更新',
- db_newer: '数据库版本更新',
- conflict: '文件内容冲突',
- };
-
return (
-
-
+
+
+
+ }
+ onClick={loadDiff}
+ >
+ 刷新
+
+
+
-
-
-
- ),
- },
- {
- key: 'local',
- label: '本地内容',
- children: (
-
- {local_content}
-
- ),
- },
- {
- key: 'db',
- label: '数据库内容',
- children: (
-
- {db_content}
-
- ),
- },
- ]}
- />
+ {STATUS_CONFIG[diffData.status] && (
+
+ )}
+
+ {renderDiff()}
);
}
\ No newline at end of file