完善对比内容

This commit is contained in:
2025-07-31 23:45:15 +08:00
parent 3c00d85302
commit 689328eeca
3 changed files with 754 additions and 29 deletions

View File

@@ -14,7 +14,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- 查询执行和结果比对算法
- 配置组管理CRUD操作
- JSON字段特殊处理和数组比较逻辑
- `config_groups.db`: SQLite数据库存储用户保存的配置组
- 查询历史记录管理
- `config_groups.db`: SQLite数据库存储用户保存的配置组和查询历史
**前端 (原生JavaScript + Bootstrap)**
- `templates/db_compare.html`: 主界面模板,包含配置表单和结果展示
@@ -25,6 +26,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- 原生数据展示(多种视图模式:格式化、原始、差异对比、树形)
- 高级错误处理和用户反馈
**示例代码**
- `demo/Query.py`: 独立的Cassandra查询比对脚本示例
- `demo/twcsQuery.py`: 另一个查询示例
### 关键功能模块
**数据比对引擎**
@@ -32,6 +37,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- 数组字段的顺序无关比较
- 字段级别的差异统计和分析
- 数据质量评估和建议生成
- 支持包含和排除特定字段的比较
**用户界面特性**
- 分页系统(差异记录和相同记录)
@@ -39,12 +45,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- 原生数据展示JSON语法高亮、树形视图
- 配置导入导出和管理
- 详细的错误诊断和故障排查指南
- 查询历史记录和复用
## 开发相关命令
### 环境设置
```bash
# 安装依赖
# 安装依赖仅需要Flask和cassandra-driver
pip install -r requirements.txt
# 运行应用默认端口5000
@@ -57,13 +64,24 @@ python app.py
### 开发模式
应用默认运行在debug模式代码修改后自动重启。访问 http://localhost:5000 查看首页http://localhost:5000/db-compare 使用比对工具。
### 依赖项
- Flask==2.3.3
- cassandra-driver==3.29.1
## API架构说明
### 核心API端点
- `GET /api/default-config`: 获取默认数据库配置
- `POST /api/query`: 执行数据库查询比对(主要功能)
- `GET|POST|DELETE /api/config-groups`: 配置组的CRUD操作
- `GET /api/config-groups`: 获取所有配置组
- `POST /api/config-groups`: 创建新配置组
- `GET /api/config-groups/<id>`: 获取特定配置组
- `DELETE /api/config-groups/<id>`: 删除配置组
- `POST /api/init-db`: 初始化SQLite数据库
- `GET /api/query-history`: 获取查询历史
- `POST /api/query-history`: 保存查询历史
- `GET /api/query-history/<id>`: 获取特定历史记录
- `DELETE /api/query-history/<id>`: 删除历史记录
### 查询比对流程
1. 前端发送配置和Key值列表到 `/api/query`
@@ -108,6 +126,8 @@ python app.py
- 使用DCAwareRoundRobinPolicy避免负载均衡警告
- 连接超时设置为10秒
- 失败时提供网络连通性测试
- 支持认证PlainTextAuthProvider
- 支持集群配置cluster_name, datacenter
### 前端状态管理
- `currentResults`: 存储最新查询结果
@@ -129,4 +149,26 @@ python app.py
- 大数据集的分页处理
- 原生数据的延迟加载
- JSON格式化的客户端缓存
- 搜索和过滤的防抖处理
- 搜索和过滤的防抖处理
### SQLite数据库表结构
**config_groups表**
- id: 主键
- name: 配置组名称(唯一)
- description: 描述
- pro_config: 生产环境配置JSON
- test_config: 测试环境配置JSON
- query_config: 查询配置JSON
- created_at/updated_at: 时间戳
**query_history表**
- id: 主键
- name: 查询名称
- description: 描述
- pro_config/test_config/query_config: 配置JSON
- query_keys: 查询的键值JSON
- results_summary: 结果摘要JSON
- execution_time: 执行时间
- total_keys/differences_count/identical_count: 统计数据
- created_at: 时间戳

View File

@@ -600,7 +600,7 @@ function displayDifferences() {
<span class="badge bg-warning ms-2">记录缺失</span>
</div>
<div class="col-auto">
<button class="btn btn-sm btn-outline-info" onclick="showDifferenceRawData('${escapeForJs(JSON.stringify(diff.key))}')">
<button class="btn btn-sm btn-outline-info" onclick='showDifferenceRawData(${JSON.stringify(JSON.stringify(diff.key))})'>
<i class="fas fa-code"></i> 原生数据
</button>
</div>
@@ -633,7 +633,7 @@ function displayDifferences() {
data-bs-toggle="collapse" data-bs-target="#diffCollapse${globalIndex}">
<i class="fas fa-eye"></i> 查看详情
</button>
<button class="btn btn-sm btn-outline-info" onclick="showDifferenceRawData('${escapeForJs(JSON.stringify(diff.key))}')">
<button class="btn btn-sm btn-outline-info" onclick='showDifferenceRawData(${JSON.stringify(JSON.stringify(diff.key))})'>
<i class="fas fa-code"></i> 原生数据
</button>
</div>
@@ -762,7 +762,7 @@ function displayIdenticalResults() {
data-bs-toggle="collapse" data-bs-target="#collapse${globalIndex}">
<i class="fas fa-eye"></i> 查看详情
</button>
<button class="btn btn-sm btn-outline-info" onclick="showRawData('${escapeForJs(JSON.stringify(result.key))}')">
<button class="btn btn-sm btn-outline-info" onclick='showRawData(${JSON.stringify(JSON.stringify(result.key))})'>
<i class="fas fa-code"></i> 原生数据
</button>
</div>
@@ -1171,40 +1171,132 @@ function escapeForJs(str) {
.replace(/\0/g, '\\0'); // 空字符
}
// 显示原生数据(简化测试版本)
// 显示原生数据
function showRawData(keyStr) {
console.log('showRawData 开始执行,参数:', keyStr);
// 添加基本检查
if (!currentResults) {
console.error('没有查询结果数据');
alert('请先执行查询操作');
showAlert('warning', '请先执行查询操作');
return;
}
try {
console.log('开始解析 keyStr:', keyStr);
// 解析key
const key = JSON.parse(keyStr);
// 创建最简单的测试模态框
const existingModal = document.getElementById('rawDataModal');
if (existingModal) {
existingModal.remove();
// 在原生数据中查找对应的记录
let proData = null;
let testData = null;
// 查找生产环境数据
if (currentResults.raw_pro_data) {
proData = currentResults.raw_pro_data.find(item => {
// 比较主键
return JSON.stringify(item[Object.keys(key)[0]]) === JSON.stringify(key[Object.keys(key)[0]]);
});
}
// 创建基本模态框
const modalHTML = `
// 查找测试环境数据
if (currentResults.raw_test_data) {
testData = currentResults.raw_test_data.find(item => {
// 比较主键
return JSON.stringify(item[Object.keys(key)[0]]) === JSON.stringify(key[Object.keys(key)[0]]);
});
}
// 创建模态框内容
const modalContent = `
<div class="modal fade" id="rawDataModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">原生数据测试</h5>
<h5 class="modal-title">
<i class="fas fa-code"></i> 原生数据 - ${JSON.stringify(key)}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>测试模态框显示成功!</p>
<pre id="testContent">测试内容</pre>
<ul class="nav nav-tabs" id="rawDataTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="formatted-tab" data-bs-toggle="tab"
data-bs-target="#formatted" type="button" role="tab">
<i class="fas fa-code"></i> 格式化视图
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="raw-tab" data-bs-toggle="tab"
data-bs-target="#raw" type="button" role="tab">
<i class="fas fa-file-code"></i> 原始视图
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="diff-tab" data-bs-toggle="tab"
data-bs-target="#diff" type="button" role="tab">
<i class="fas fa-exchange-alt"></i> 对比视图
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tree-tab" data-bs-toggle="tab"
data-bs-target="#tree" type="button" role="tab">
<i class="fas fa-tree"></i> 树形视图
</button>
</li>
<li class="nav-item ms-auto">
<span class="nav-link text-muted">
<i class="fas fa-sync-alt"></i> 同步滚动已启用
</span>
</li>
</ul>
<div class="tab-content mt-3" id="rawDataTabsContent">
<!-- 格式化视图 -->
<div class="tab-pane fade show active" id="formatted" role="tabpanel">
<div class="row">
<div class="col-md-6">
<h6 class="text-primary"><i class="fas fa-database"></i> 生产环境数据</h6>
<pre class="bg-light p-3 rounded sync-scroll" style="max-height: 500px; overflow-y: auto;">${proData ? escapeHtml(JSON.stringify(proData, null, 2)) : '无数据'}</pre>
</div>
<div class="col-md-6">
<h6 class="text-secondary"><i class="fas fa-database"></i> 测试环境数据</h6>
<pre class="bg-light p-3 rounded sync-scroll" style="max-height: 500px; overflow-y: auto;">${testData ? escapeHtml(JSON.stringify(testData, null, 2)) : '无数据'}</pre>
</div>
</div>
</div>
<!-- 原始视图 -->
<div class="tab-pane fade" id="raw" role="tabpanel">
<div class="mb-3">
<h6 class="text-primary"><i class="fas fa-database"></i> 生产环境原始数据</h6>
<pre class="bg-light p-3 rounded" style="max-height: 300px; overflow-y: auto;">${proData ? escapeHtml(JSON.stringify(proData)) : '无数据'}</pre>
</div>
<div>
<h6 class="text-secondary"><i class="fas fa-database"></i> 测试环境原始数据</h6>
<pre class="bg-light p-3 rounded" style="max-height: 300px; overflow-y: auto;">${testData ? escapeHtml(JSON.stringify(testData)) : '无数据'}</pre>
</div>
</div>
<!-- 对比视图 -->
<div class="tab-pane fade" id="diff" role="tabpanel">
<div id="diffView"></div>
</div>
<!-- 树形视图 -->
<div class="tab-pane fade" id="tree" role="tabpanel">
<div class="row">
<div class="col-md-6">
<h6 class="text-primary"><i class="fas fa-database"></i> 生产环境数据</h6>
<div id="proTreeView" class="tree-view"></div>
</div>
<div class="col-md-6">
<h6 class="text-secondary"><i class="fas fa-database"></i> 测试环境数据</h6>
<div id="testTreeView" class="tree-view"></div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="copyRawData()">
<i class="fas fa-copy"></i> 复制数据
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
@@ -1212,18 +1304,40 @@ function showRawData(keyStr) {
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
// 移除现有modal
const existingModal = document.getElementById('rawDataModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalContent);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('rawDataModal'));
modal.show();
console.log('测试模态框显示成功');
// 保存当前数据供复制使用
window.currentRawData = {
pro: proData ? JSON.stringify(proData, null, 2) : '无数据',
test: testData ? JSON.stringify(testData, null, 2) : '无数据',
combined: `生产环境数据:\n${proData ? JSON.stringify(proData, null, 2) : '无数据'}\n\n测试环境数据:\n${testData ? JSON.stringify(testData, null, 2) : '无数据'}`
};
// 延迟渲染对比视图和树形视图
setTimeout(() => {
renderDiffView(proData, testData);
renderTreeView('proTreeView', proData);
renderTreeView('testTreeView', testData);
// 为格式化视图添加同步滚动
setupSyncScroll('formatted');
// 为树形视图添加同步滚动
setupSyncScroll('tree');
}, 100);
} catch (error) {
console.error('showRawData 执行失败:', error);
console.error('错误堆栈:', error.stack);
alert('错误: ' + error.message);
console.error('显示原生数据失败:', error);
showAlert('danger', '显示原生数据失败: ' + error.message);
}
}
@@ -1249,6 +1363,493 @@ function showDifferenceRawData(keyStr) {
showRawData(keyStr); // 复用相同的原生数据显示逻辑
}
// 渲染对比视图
function renderDiffView(proData, testData) {
const diffViewContainer = document.getElementById('diffView');
if (!proData && !testData) {
diffViewContainer.innerHTML = '<p class="text-muted text-center">无数据可对比</p>';
return;
}
// 获取所有字段
const allFields = new Set([
...(proData ? Object.keys(proData) : []),
...(testData ? Object.keys(testData) : [])
]);
// 分离相同和不同的字段
const differentFields = [];
const identicalFields = [];
allFields.forEach(field => {
const proValue = proData ? proData[field] : undefined;
const testValue = testData ? testData[field] : undefined;
// 特殊处理:区分 null 和 undefined
// undefined 表示字段不存在null 表示字段存在但值为 null
let isEqual = false;
// 如果一个是 undefined另一个不是包括 null则不相等
if (proValue === undefined && testValue !== undefined) {
isEqual = false;
} else if (proValue !== undefined && testValue === undefined) {
isEqual = false;
} else if (proValue === testValue) {
// 两个都是 undefined 或者值完全相同
isEqual = true;
} else if (proValue !== undefined && testValue !== undefined) {
// 两个都有值,尝试规范化比较
try {
const proNormalized = normalizeValue(proValue);
const testNormalized = normalizeValue(testValue);
isEqual = JSON.stringify(proNormalized) === JSON.stringify(testNormalized);
} catch (e) {
isEqual = JSON.stringify(proValue) === JSON.stringify(testValue);
}
}
if (isEqual) {
identicalFields.push(field);
} else {
differentFields.push(field);
}
});
let html = '';
// 如果有差异字段,先显示差异统计
if (differentFields.length > 0) {
html += `
<div class="alert alert-warning mb-3">
<i class="fas fa-exclamation-triangle"></i>
发现 <strong>${differentFields.length}</strong> 个字段存在差异
</div>
`;
}
html += '<div class="table-responsive"><table class="table table-bordered table-hover">';
html += `
<thead class="table-dark">
<tr>
<th style="width: 15%;">字段名</th>
<th style="width: 40%;">生产环境</th>
<th style="width: 40%;">测试环境</th>
<th style="width: 5%;" class="text-center">状态</th>
</tr>
</thead>
<tbody>
`;
// 先显示差异字段(重点突出)
if (differentFields.length > 0) {
html += '<tr class="table-danger"><td colspan="4" class="text-center fw-bold">差异字段</td></tr>';
differentFields.forEach(field => {
const proValue = proData ? proData[field] : undefined;
const testValue = testData ? testData[field] : undefined;
// 判断值的类型和差异类型
let diffType = '';
let proDisplay = '';
let testDisplay = '';
if (proValue === undefined && testValue !== undefined) {
diffType = 'missing-in-pro';
proDisplay = '<span class="text-danger"><i class="fas fa-minus-circle"></i> 缺失</span>';
testDisplay = formatValue(testValue);
} else if (proValue !== undefined && testValue === undefined) {
diffType = 'missing-in-test';
proDisplay = formatValue(proValue);
testDisplay = '<span class="text-danger"><i class="fas fa-minus-circle"></i> 缺失</span>';
} else {
diffType = 'value-diff';
proDisplay = formatValue(proValue);
testDisplay = formatValue(testValue);
}
html += `
<tr class="table-warning">
<td>
<code class="text-danger fw-bold">${field}</code>
${getFieldTypeIcon(proValue || testValue)}
</td>
<td class="font-monospace small ${diffType === 'missing-in-pro' ? 'bg-light' : ''}" style="vertical-align: top;">${proDisplay}</td>
<td class="font-monospace small ${diffType === 'missing-in-test' ? 'bg-light' : ''}" style="vertical-align: top;">${testDisplay}</td>
<td class="text-center" style="vertical-align: middle;">
<span class="badge bg-danger">
<i class="fas fa-times"></i> 不同
</span>
</td>
</tr>
`;
});
}
// 再显示相同字段
if (identicalFields.length > 0) {
html += '<tr class="table-success"><td colspan="4" class="text-center fw-bold">相同字段</td></tr>';
identicalFields.forEach(field => {
const value = proData ? proData[field] : testData[field];
const displayValue = formatValue(value);
html += `
<tr>
<td>
<code class="text-muted">${field}</code>
${getFieldTypeIcon(value)}
</td>
<td class="font-monospace small text-muted">${displayValue}</td>
<td class="font-monospace small text-muted">${displayValue}</td>
<td class="text-center">
<span class="badge bg-success">
<i class="fas fa-check"></i> 相同
</span>
</td>
</tr>
`;
});
}
html += '</tbody></table></div>';
// 添加快速跳转按钮
if (differentFields.length > 0 && identicalFields.length > 0) {
html = `
<div class="d-flex gap-2 mb-3">
<button class="btn btn-sm btn-warning" onclick="scrollToTableSection('danger')">
<i class="fas fa-exclamation-triangle"></i> 跳转到差异字段
</button>
<button class="btn btn-sm btn-success" onclick="scrollToTableSection('success')">
<i class="fas fa-check"></i> 跳转到相同字段
</button>
</div>
` + html;
}
diffViewContainer.innerHTML = html;
}
// 格式化值显示
function formatValue(value) {
if (value === null) return '<span class="text-muted">null</span>';
if (value === undefined) return '<span class="text-muted">undefined</span>';
if (value === '') return '<span class="text-muted">(空字符串)</span>';
const type = typeof value;
if (type === 'object') {
// 特殊处理数组
if (Array.isArray(value)) {
// 检查是否是JSON字符串数组
let isJsonStringArray = value.length > 0 && value.every(item => {
if (typeof item === 'string') {
try {
if ((item.startsWith('{') && item.endsWith('}')) ||
(item.startsWith('[') && item.endsWith(']'))) {
JSON.parse(item);
return true;
}
} catch (e) {}
}
return false;
});
if (isJsonStringArray) {
// 解析并格式化JSON字符串数组
try {
const parsedArray = value.map(item => JSON.parse(item));
// 如果都有key字段按key排序
if (parsedArray.every(item => 'key' in item)) {
parsedArray.sort((a, b) => a.key - b.key);
}
const formatted = JSON.stringify(parsedArray, null, 2);
return `<pre class="mb-0 p-2 bg-light rounded" style="max-height: 400px; overflow-y: auto; white-space: pre-wrap;">${escapeHtml(formatted)}</pre>`;
} catch (e) {
// 如果解析失败,按原样显示
}
}
}
// 对象或数组,格式化显示
try {
const formatted = JSON.stringify(value, null, 2);
return `<pre class="mb-0 p-2 bg-light rounded" style="max-height: 300px; overflow-y: auto; white-space: pre-wrap;">${escapeHtml(formatted)}</pre>`;
} catch (e) {
return `<pre class="mb-0 p-2 bg-light rounded">${escapeHtml(String(value))}</pre>`;
}
}
if (type === 'string') {
// 检查是否是JSON字符串
try {
if ((value.startsWith('{') && value.endsWith('}')) ||
(value.startsWith('[') && value.endsWith(']'))) {
const parsed = JSON.parse(value);
const formatted = JSON.stringify(parsed, null, 2);
return `<pre class="mb-0 p-2 bg-light rounded" style="max-height: 300px; overflow-y: auto; white-space: pre-wrap;">${escapeHtml(formatted)}</pre>`;
}
} catch (e) {
// 不是有效的JSON作为普通字符串处理
}
// 长文本处理
if (value.length > 200) {
return `<div class="text-wrap" style="max-width: 400px; max-height: 200px; overflow-y: auto;">${escapeHtml(value)}</div>`;
}
}
return escapeHtml(String(value));
}
// 获取字段类型图标
function getFieldTypeIcon(value) {
if (value === undefined) {
return '<span class="badge bg-danger ms-2" title="字段缺失">❌</span>';
}
if (value === null) {
return '<span class="badge bg-secondary ms-2" title="空值">NULL</span>';
}
const type = typeof value;
if (Array.isArray(value)) {
return '<span class="badge bg-info ms-2" title="数组">[]</span>';
}
if (type === 'object') {
return '<span class="badge bg-primary ms-2" title="对象">{}</span>';
}
if (type === 'string') {
// 检查是否是JSON字符串
try {
if (value.startsWith('{') || value.startsWith('[')) {
JSON.parse(value);
return '<span class="badge bg-warning ms-2" title="JSON字符串">JSON</span>';
}
} catch (e) {
// 不是JSON
}
// 检查是否是日期字符串
if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
return '<span class="badge bg-secondary ms-2" title="日期">📅</span>';
}
}
if (type === 'number') {
return '<span class="badge bg-success ms-2" title="数字">#</span>';
}
if (type === 'boolean') {
return '<span class="badge bg-dark ms-2" title="布尔值">⚡</span>';
}
return '';
}
// 滚动到表格指定部分
function scrollToTableSection(sectionType) {
const targetRow = document.querySelector(`#diffView .table-${sectionType}`);
if (targetRow) {
targetRow.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
// 规范化值用于比较处理JSON键顺序问题
function normalizeValue(value) {
if (typeof value === 'string') {
// 尝试解析JSON字符串
try {
if ((value.startsWith('{') && value.endsWith('}')) ||
(value.startsWith('[') && value.endsWith(']'))) {
value = JSON.parse(value);
} else {
return value;
}
} catch (e) {
return value;
}
}
if (value === null || typeof value !== 'object') {
return value;
}
if (Array.isArray(value)) {
// 对于数组,需要特殊处理
// 1. 先尝试解析每个元素可能是JSON字符串
const parsedArray = value.map(item => {
if (typeof item === 'string') {
try {
if ((item.startsWith('{') && item.endsWith('}')) ||
(item.startsWith('[') && item.endsWith(']'))) {
return JSON.parse(item);
}
} catch (e) {
// 保持原始字符串
}
}
return item;
});
// 2. 检查是否是对象数组,如果是,可能需要按某个键排序
if (parsedArray.length > 0 && typeof parsedArray[0] === 'object' && !Array.isArray(parsedArray[0])) {
// 检查是否所有元素都有相同的键
const firstKeys = Object.keys(parsedArray[0]).sort().join(',');
const allSameStructure = parsedArray.every(item =>
typeof item === 'object' &&
!Array.isArray(item) &&
Object.keys(item).sort().join(',') === firstKeys
);
if (allSameStructure) {
// 如果有 key 字段,按 key 排序
if ('key' in parsedArray[0]) {
return parsedArray
.map(item => normalizeValue(item))
.sort((a, b) => {
const aKey = a.key;
const bKey = b.key;
if (aKey < bKey) return -1;
if (aKey > bKey) return 1;
return 0;
});
}
// 否则按整个对象的字符串表示排序
return parsedArray
.map(item => normalizeValue(item))
.sort((a, b) => {
const aStr = JSON.stringify(a);
const bStr = JSON.stringify(b);
if (aStr < bStr) return -1;
if (aStr > bStr) return 1;
return 0;
});
}
}
// 递归处理每个元素
return parsedArray.map(item => normalizeValue(item));
}
// 对象:按键排序
const sortedObj = {};
Object.keys(value).sort().forEach(key => {
sortedObj[key] = normalizeValue(value[key]);
});
return sortedObj;
}
// 渲染树形视图
function renderTreeView(containerId, data) {
const container = document.getElementById(containerId);
if (!data) {
container.innerHTML = '<p class="text-muted">无数据</p>';
return;
}
container.innerHTML = createTreeNode(data, '');
}
// 创建树形节点
function createTreeNode(obj, indent = '') {
if (obj === null) return '<span class="text-muted">null</span>';
if (obj === undefined) return '<span class="text-muted">undefined</span>';
const type = typeof obj;
if (type !== 'object') {
return `<span class="tree-value">${escapeHtml(JSON.stringify(obj))}</span>`;
}
if (Array.isArray(obj)) {
if (obj.length === 0) return '<span class="text-muted">[]</span>';
let html = '<div class="tree-array">[<br>';
obj.forEach((item, index) => {
html += `${indent} <span class="tree-index">${index}:</span> ${createTreeNode(item, indent + ' ')}`;
if (index < obj.length - 1) html += ',';
html += '<br>';
});
html += `${indent}]</div>`;
return html;
}
const keys = Object.keys(obj);
if (keys.length === 0) return '<span class="text-muted">{}</span>';
let html = '<div class="tree-object">{<br>';
keys.forEach((key, index) => {
html += `${indent} <span class="tree-key">"${key}"</span>: ${createTreeNode(obj[key], indent + ' ')}`;
if (index < keys.length - 1) html += ',';
html += '<br>';
});
html += `${indent}}</div>`;
return html;
}
// 设置同步滚动
function setupSyncScroll(tabType) {
if (tabType === 'formatted') {
// 格式化视图的两个pre元素
const proPre = document.querySelector('#formatted .col-md-6:first-child pre');
const testPre = document.querySelector('#formatted .col-md-6:last-child pre');
if (proPre && testPre) {
let syncing = false;
proPre.addEventListener('scroll', () => {
if (!syncing) {
syncing = true;
testPre.scrollTop = proPre.scrollTop;
testPre.scrollLeft = proPre.scrollLeft;
setTimeout(() => syncing = false, 10);
}
});
testPre.addEventListener('scroll', () => {
if (!syncing) {
syncing = true;
proPre.scrollTop = testPre.scrollTop;
proPre.scrollLeft = testPre.scrollLeft;
setTimeout(() => syncing = false, 10);
}
});
}
} else if (tabType === 'tree') {
// 树形视图的两个div元素
const proTree = document.getElementById('proTreeView');
const testTree = document.getElementById('testTreeView');
if (proTree && testTree) {
let syncing = false;
proTree.addEventListener('scroll', () => {
if (!syncing) {
syncing = true;
testTree.scrollTop = proTree.scrollTop;
testTree.scrollLeft = proTree.scrollLeft;
setTimeout(() => syncing = false, 10);
}
});
testTree.addEventListener('scroll', () => {
if (!syncing) {
syncing = true;
proTree.scrollTop = testTree.scrollTop;
proTree.scrollLeft = testTree.scrollLeft;
setTimeout(() => syncing = false, 10);
}
});
}
}
}
// 显示比较总结
function displayComparisonSummary(summary) {
const summaryContainer = document.getElementById('comparison-summary');

View File

@@ -144,6 +144,88 @@
outline: 0;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
/* 树形视图样式 */
.tree-view {
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
padding: 15px;
background-color: #f8f9fa;
border-radius: 5px;
overflow-x: auto;
max-height: 500px;
overflow-y: auto;
}
.tree-key {
color: #0066cc;
font-weight: bold;
}
.tree-value {
color: #008000;
}
.tree-index {
color: #666;
font-style: italic;
}
.tree-object, .tree-array {
margin-left: 20px;
}
/* 原生数据模态框样式 */
#rawDataModal .modal-dialog {
max-width: 90%;
}
#rawDataModal pre {
white-space: pre-wrap;
word-wrap: break-word;
}
#rawDataModal .nav-tabs .nav-link {
color: #495057;
}
#rawDataModal .nav-tabs .nav-link.active {
font-weight: bold;
}
/* 对比视图增强样式 */
#diffView .table-warning td {
background-color: #fff3cd !important;
border-color: #ffc107;
}
#diffView .table-danger td {
background-color: #f8d7da !important;
font-weight: bold;
color: #721c24;
}
#diffView .table-success td {
background-color: #d4edda !important;
color: #155724;
}
#diffView .badge {
font-size: 0.7rem;
}
#diffView .text-truncate {
cursor: pointer;
}
#diffView .text-truncate:hover {
background-color: rgba(0,0,0,0.05);
padding: 2px 4px;
border-radius: 3px;
}
#diffView pre {
font-size: 12px;
line-height: 1.4;
margin: 0;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
}
#diffView .table-warning pre {
background-color: #fff;
border-color: #ffc107;
}
#diffView td {
padding: 0.5rem;
}
</style>
</head>
<body>