完善对比内容
This commit is contained in:
50
CLAUDE.md
50
CLAUDE.md
@@ -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: 时间戳
|
651
static/js/app.js
651
static/js/app.js
@@ -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');
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user