Files
BigDataTool/static/js/redis_compare.js
2025-08-04 21:55:35 +08:00

1870 lines
62 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Redis比较工具的JavaScript功能
* 支持Redis集群配置、连接测试、数据比较等功能
*/
// 全局变量
let currentResults = null;
let isQuerying = false;
let currentImportTarget = null; // 记录当前导入目标集群
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initializePage();
bindEvents();
loadRedisConfigGroups();
loadRedisQueryHistory();
});
/**
* 初始化页面
*/
function initializePage() {
// 加载默认配置
loadDefaultConfig();
// 绑定节点删除事件
bindNodeEvents();
// 绑定查询模式切换事件
bindQueryModeEvents();
}
/**
* 绑定事件
*/
function bindEvents() {
// 查询模式切换
document.querySelectorAll('input[name="queryMode"]').forEach(radio => {
radio.addEventListener('change', toggleQueryMode);
});
// 节点删除按钮事件委托
document.addEventListener('click', function(e) {
if (e.target.closest('.remove-node')) {
e.target.closest('.node-input').remove();
}
});
}
/**
* 绑定节点相关事件
*/
function bindNodeEvents() {
// 初始绑定现有的删除按钮(如果只有一个节点则禁用删除)
updateNodeDeleteButtons();
}
/**
* 绑定查询模式切换事件
*/
function bindQueryModeEvents() {
const randomMode = document.getElementById('randomMode');
const specifiedMode = document.getElementById('specifiedMode');
randomMode.addEventListener('change', toggleQueryMode);
specifiedMode.addEventListener('change', toggleQueryMode);
}
/**
* 切换查询模式
*/
function toggleQueryMode() {
const randomMode = document.getElementById('randomMode').checked;
const randomOptions = document.getElementById('randomOptions');
const specifiedOptions = document.getElementById('specifiedOptions');
if (randomMode) {
randomOptions.style.display = 'block';
specifiedOptions.style.display = 'none';
} else {
randomOptions.style.display = 'none';
specifiedOptions.style.display = 'block';
}
}
/**
* 添加节点
*/
function addNode(clusterId) {
const container = document.getElementById(`${clusterId}Nodes`);
if (!container) {
console.error(`节点容器未找到: ${clusterId}Nodes`);
return;
}
const nodeInput = document.createElement('div');
nodeInput.className = 'node-input';
nodeInput.innerHTML = `
<input type="text" class="form-control node-host" placeholder="主机地址" value="127.0.0.1">
<input type="number" class="form-control node-port" placeholder="端口" value="7000" style="width: 100px;">
<button type="button" class="btn btn-outline-danger btn-sm remove-node">
<i class="fas fa-minus"></i>
</button>
`;
container.appendChild(nodeInput);
updateNodeDeleteButtons();
}
/**
* 更新节点删除按钮状态
*/
function updateNodeDeleteButtons() {
// 每个集群至少需要一个节点
['cluster1', 'cluster2'].forEach(clusterId => {
const container = document.getElementById(`${clusterId}Nodes`);
if (container) {
const nodeInputs = container.querySelectorAll('.node-input');
const deleteButtons = container.querySelectorAll('.remove-node');
deleteButtons.forEach(btn => {
btn.disabled = nodeInputs.length <= 1;
});
}
});
}
/**
* 获取集群配置
*/
function getClusterConfig(clusterId) {
const name = document.getElementById(`${clusterId}Name`).value;
const password = document.getElementById(`${clusterId}Password`).value;
// 获取节点列表
const nodes = [];
const nodeInputs = document.querySelectorAll(`#${clusterId}Nodes .node-input`);
nodeInputs.forEach(nodeInput => {
const host = nodeInput.querySelector('input[type="text"]').value.trim();
const port = parseInt(nodeInput.querySelector('input[type="number"]').value);
if (host && port) {
nodes.push({ host, port });
}
});
return {
name,
nodes,
password: password || null,
socket_timeout: 3,
socket_connect_timeout: 3,
max_connections_per_node: 16
};
}
/**
* 获取查询选项
*/
function getQueryOptions() {
const randomMode = document.getElementById('randomMode').checked;
if (randomMode) {
return {
mode: 'random',
count: parseInt(document.getElementById('sampleCount').value),
pattern: document.getElementById('keyPattern').value,
source_cluster: document.getElementById('sourceCluster').value
};
} else {
const keysText = document.getElementById('specifiedKeys').value.trim();
const keys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : [];
return {
mode: 'specified',
keys: keys
};
}
}
/**
* 测试连接
*/
async function testConnection(clusterId) {
const config = getClusterConfig(clusterId);
const clusterName = config.name;
if (!config.nodes || config.nodes.length === 0) {
showAlert('请至少配置一个Redis节点', 'warning');
return;
}
try {
showLoading(`正在测试${clusterName}连接...`);
const response = await fetch('/api/redis/test-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
cluster_config: config,
cluster_name: clusterName
})
});
const result = await response.json();
hideLoading();
if (result.success) {
showAlert(`${clusterName}连接成功!连接耗时: ${result.data.connection_time.toFixed(3)}`, 'success');
// 高亮成功的集群配置 - 使用正确的ID选择器
const configElement = document.querySelector(`.cluster-config[data-cluster="${clusterId}"]`) ||
document.querySelector(`#${clusterId}Config`) ||
document.querySelector(`[id*="${clusterId}"]`);
if (configElement) {
configElement.classList.add('active');
setTimeout(() => {
configElement.classList.remove('active');
}, 2000);
}
} else {
showAlert(`${clusterName}连接失败: ${result.error}`, 'danger');
}
} catch (error) {
hideLoading();
showAlert(`连接测试失败: ${error.message}`, 'danger');
}
}
/**
* 执行Redis比较
*/
async function executeRedisComparison() {
if (isQuerying) {
showAlert('查询正在进行中,请稍候...', 'warning');
return;
}
try {
// 获取配置
const cluster1Config = getClusterConfig('cluster1');
const cluster2Config = getClusterConfig('cluster2');
const queryOptions = getQueryOptions();
// 验证配置
if (!cluster1Config.nodes || cluster1Config.nodes.length === 0) {
showAlert('请配置集群1的Redis节点', 'warning');
return;
}
if (!cluster2Config.nodes || cluster2Config.nodes.length === 0) {
showAlert('请配置集群2的Redis节点', 'warning');
return;
}
if (queryOptions.mode === 'specified' && (!queryOptions.keys || queryOptions.keys.length === 0)) {
showAlert('请输入要查询的Key列表', 'warning');
return;
}
isQuerying = true;
showLoading('正在执行Redis数据比较请稍候...');
clearResults();
console.log('发送Redis比较请求...');
console.log('集群1配置:', cluster1Config);
console.log('集群2配置:', cluster2Config);
console.log('查询选项:', queryOptions);
const response = await fetch('/api/redis/compare', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
cluster1_config: cluster1Config,
cluster2_config: cluster2Config,
query_options: queryOptions
})
});
console.log('Redis比较API响应状态:', response.status);
const result = await response.json();
console.log('Redis比较API响应结果:', result);
if (response.ok && result.success !== false) {
currentResults = result;
displayResults(result);
showAlert('Redis数据比较完成历史记录已自动保存', 'success');
// 刷新历史记录列表(后台已自动保存)
loadRedisQueryHistory();
} else {
const errorMsg = result.error || `HTTP ${response.status} 错误`;
console.error('Redis比较失败:', errorMsg);
// 为常见错误提供友好的提示
if (errorMsg.includes('连接失败')) {
showAlert(`Redis比较失败: ${errorMsg}。请检查Redis服务器是否运行网络连接是否正常。`, 'danger');
} else {
showAlert(`Redis比较失败: ${errorMsg}`, 'danger');
}
}
} catch (error) {
console.error('Redis比较请求异常:', error);
showAlert(`请求失败: ${error.message}。请检查网络连接和服务器状态。`, 'danger');
} finally {
isQuerying = false;
hideLoading();
}
}
/**
* 显示结果
*/
function displayResults(results) {
// 显示统计卡片
displayStatsCards(results.stats);
// 显示详细结果
displayDifferenceResults(results.different_results || []);
displayIdenticalResults(results.identical_results || []);
displayMissingResults(results.missing_results || []);
displayPerformanceReport(results.performance_report);
// 显示原生数据
displayRawData(results);
// 更新标签页计数
updateTabCounts(results);
// 显示结果区域 - 使用正确的ID
const resultSection = document.getElementById('resultSection');
if (resultSection) {
resultSection.style.display = 'block';
// 滚动到结果区域
resultSection.scrollIntoView({ behavior: 'smooth' });
}
}
/**
* 显示统计卡片
*/
function displayStatsCards(stats) {
const container = document.getElementById('stats');
if (!container) {
console.error('统计卡片容器未找到');
return;
}
container.innerHTML = `
<div class="col-md-3">
<div class="stat-card bg-primary text-white">
<h3>${stats.total_keys}</h3>
<p>总Key数量</p>
</div>
</div>
<div class="col-md-3">
<div class="stat-card bg-success text-white">
<h3>${stats.identical_count}</h3>
<p>相同数据</p>
<small>${stats.identical_percentage}%</small>
</div>
</div>
<div class="col-md-3">
<div class="stat-card bg-danger text-white">
<h3>${stats.different_count}</h3>
<p>差异数据</p>
<small>${stats.different_percentage}%</small>
</div>
</div>
<div class="col-md-3">
<div class="stat-card bg-warning text-white">
<h3>${stats.missing_in_cluster1 + stats.missing_in_cluster2 + stats.both_missing}</h3>
<p>缺失数据</p>
<small>${stats.missing_percentage}%</small>
</div>
</div>
`;
}
/**
* 显示差异结果
*/
function displayDifferenceResults(differences) {
const container = document.getElementById('differences-content');
if (!container) {
console.error('差异结果容器未找到');
return;
}
if (differences.length === 0) {
container.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>未发现数据差异</div>';
return;
}
let html = '';
differences.forEach((diff, index) => {
html += `
<div class="difference-item">
<h6><i class="fas fa-key me-2"></i>Key: <code>${diff.key}</code></h6>
<div class="row">
<div class="col-md-6">
<strong>${currentResults.clusters.cluster1_name || '集群1'}:</strong>
<pre class="redis-value mt-2">${formatRedisValue(diff.cluster1_value)}</pre>
</div>
<div class="col-md-6">
<strong>${currentResults.clusters.cluster2_name || '集群2'}:</strong>
<pre class="redis-value mt-2">${formatRedisValue(diff.cluster2_value)}</pre>
</div>
</div>
<div class="mt-2">
<span class="badge bg-danger">${diff.message}</span>
</div>
</div>
`;
});
container.innerHTML = html;
}
/**
* 显示相同结果
*/
function displayIdenticalResults(identical) {
const container = document.getElementById('identical-content');
if (!container) {
console.error('相同结果容器未找到');
return;
}
if (identical.length === 0) {
container.innerHTML = '<div class="alert alert-info"><i class="fas fa-info-circle me-2"></i>没有相同的数据</div>';
return;
}
let html = '';
identical.forEach((item, index) => {
html += `
<div class="identical-item">
<h6><i class="fas fa-key me-2"></i>Key: <code>${item.key}</code></h6>
<div class="mt-2">
<strong>值:</strong>
<pre class="redis-value mt-2">${formatRedisValue(item.value)}</pre>
</div>
<div class="mt-2">
<span class="badge bg-success">数据一致</span>
</div>
</div>
`;
});
container.innerHTML = html;
}
/**
* 显示缺失结果
*/
function displayMissingResults(missing) {
const container = document.getElementById('missing-content');
if (!container) {
console.error('缺失结果容器未找到');
return;
}
if (missing.length === 0) {
container.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>没有缺失的数据</div>';
return;
}
let html = '';
missing.forEach((item, index) => {
html += `
<div class="missing-item">
<h6><i class="fas fa-key me-2"></i>Key: <code>${item.key}</code></h6>
<div class="mt-2">
<span class="badge bg-warning">${item.message}</span>
</div>
${item.cluster1_value !== undefined ? `
<div class="mt-2">
<strong>${currentResults.clusters.cluster1_name || '集群1'}:</strong>
<pre class="redis-value mt-1">${formatRedisValue(item.cluster1_value)}</pre>
</div>
` : ''}
${item.cluster2_value !== undefined ? `
<div class="mt-2">
<strong>${currentResults.clusters.cluster2_name || '集群2'}:</strong>
<pre class="redis-value mt-1">${formatRedisValue(item.cluster2_value)}</pre>
</div>
` : ''}
</div>
`;
});
container.innerHTML = html;
}
/**
* 显示性能报告
*/
function displayPerformanceReport(performanceReport) {
const container = document.getElementById('performanceReport');
const connections = performanceReport.connections || {};
const operations = performanceReport.operations || {};
let html = `
<h5><i class="fas fa-chart-line me-2"></i>性能统计报告</h5>
<div class="row">
<div class="col-md-6">
<h6>连接统计</h6>
<table class="table table-sm">
<thead>
<tr>
<th>集群</th>
<th>状态</th>
<th>耗时</th>
</tr>
</thead>
<tbody>
`;
Object.entries(connections).forEach(([clusterName, status]) => {
const statusClass = status.success ? 'success' : 'danger';
const statusText = status.success ? '成功' : '失败';
html += `
<tr>
<td>${clusterName}</td>
<td><span class="badge bg-${statusClass}">${statusText}</span></td>
<td>${status.connect_time.toFixed(3)}s</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
<div class="col-md-6">
<h6>操作统计</h6>
<table class="table table-sm">
<thead>
<tr>
<th>操作</th>
<th>耗时</th>
</tr>
</thead>
<tbody>
`;
if (operations.scan_time > 0) {
html += `<tr><td>扫描Keys</td><td>${operations.scan_time.toFixed(3)}s</td></tr>`;
}
Object.entries(operations.queries || {}).forEach(([operation, duration]) => {
html += `<tr><td>${operation}</td><td>${duration.toFixed(3)}s</td></tr>`;
});
if (operations.comparison_time > 0) {
html += `<tr><td>数据比对</td><td>${operations.comparison_time.toFixed(3)}s</td></tr>`;
}
html += `
<tr class="table-primary">
<td><strong>总耗时</strong></td>
<td><strong>${performanceReport.total_time.toFixed(3)}s</strong></td>
</tr>
</tbody>
</table>
</div>
</div>
`;
container.innerHTML = html;
}
/**
* 更新标签页计数
*/
function updateTabCounts(results) {
document.getElementById('diff-count').textContent = (results.different_results || []).length;
document.getElementById('identical-count').textContent = (results.identical_results || []).length;
document.getElementById('missing-count').textContent = (results.missing_results || []).length;
}
/**
* 格式化Redis值显示
*/
function formatRedisValue(value) {
if (value === null) {
return '(null)';
}
if (value === undefined) {
return '(undefined)';
}
// 如果是字符串且看起来像JSON尝试格式化
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value);
return JSON.stringify(parsed, null, 2);
} catch (e) {
// 不是JSON直接返回
return value;
}
}
return String(value);
}
/**
* 加载默认配置
*/
async function loadDefaultConfig() {
try {
const response = await fetch('/api/redis/default-config');
const config = await response.json();
// 这里可以根据需要设置默认值当前HTML已经包含了合理的默认值
} catch (error) {
console.warn('加载默认配置失败:', error);
}
}
/**
* 清空结果
*/
function clearResults() {
const resultSection = document.getElementById('resultSection');
if (resultSection) {
resultSection.style.display = 'none';
}
currentResults = null;
}
/**
* 显示加载状态
*/
function showLoading(message = '正在处理...') {
const loadingElement = document.querySelector('.loading') || document.getElementById('loadingIndicator');
if (loadingElement) {
const messageElement = loadingElement.querySelector('span') || loadingElement.querySelector('#loadingText');
if (messageElement) {
messageElement.textContent = message;
}
loadingElement.style.display = 'block';
}
}
/**
* 隐藏加载状态
*/
function hideLoading() {
const loadingElement = document.querySelector('.loading') || document.getElementById('loadingIndicator');
if (loadingElement) {
loadingElement.style.display = 'none';
}
}
/**
* 显示提示消息
*/
function showAlert(message, type = 'info') {
// 移除现有的alert
const existingAlert = document.querySelector('.alert-custom');
if (existingAlert) {
existingAlert.remove();
}
// 创建新的alert
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show alert-custom`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// 插入到页面顶部
const container = document.querySelector('.container');
container.insertBefore(alertDiv, container.firstChild);
// 5秒后自动消失
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
/**
* Redis配置管理功能
*/
// 加载Redis配置组列表
async function loadRedisConfigGroups() {
try {
const response = await fetch('/api/redis/config-groups');
const result = await response.json();
const select = document.getElementById('redisConfigGroupSelect');
if (!select) {
console.error('未找到Redis配置组下拉框元素: redisConfigGroupSelect');
return;
}
select.innerHTML = '<option value="">选择Redis配置组...</option>';
if (result.success && result.data) {
result.data.forEach(group => {
const option = document.createElement('option');
option.value = group.id;
option.textContent = `${group.name} - ${group.description || '无描述'}`;
select.appendChild(option);
});
console.log(`成功加载 ${result.data.length} 个Redis配置组`);
} else {
console.log('没有找到Redis配置组数据');
}
} catch (error) {
console.error('加载Redis配置组失败:', error);
showAlert('加载Redis配置组失败: ' + error.message, 'danger');
}
}
// 显示保存Redis配置对话框
function showSaveRedisConfigDialog() {
// 生成默认名称
const timestamp = new Date().toLocaleString('zh-CN');
document.getElementById('redisConfigName').value = `Redis配置_${timestamp}`;
document.getElementById('redisConfigDescription').value = '';
new bootstrap.Modal(document.getElementById('saveRedisConfigModal')).show();
}
// 保存Redis配置组
async function saveRedisConfigGroup() {
const name = document.getElementById('redisConfigName').value.trim();
const description = document.getElementById('redisConfigDescription').value.trim();
if (!name) {
showAlert('请输入配置组名称', 'warning');
return;
}
const cluster1Config = getClusterConfig('cluster1');
const cluster2Config = getClusterConfig('cluster2');
const queryOptions = getQueryOptions();
if (!cluster1Config.nodes || cluster1Config.nodes.length === 0) {
showAlert('请配置集群1信息', 'warning');
return;
}
if (!cluster2Config.nodes || cluster2Config.nodes.length === 0) {
showAlert('请配置集群2信息', 'warning');
return;
}
try {
const response = await fetch('/api/redis/config-groups', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
description: description,
cluster1_config: cluster1Config,
cluster2_config: cluster2Config,
query_options: queryOptions
})
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
bootstrap.Modal.getInstance(document.getElementById('saveRedisConfigModal')).hide();
loadRedisConfigGroups(); // 重新加载配置组列表
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`保存失败: ${error.message}`, 'danger');
}
}
// 加载选定的Redis配置组
async function loadSelectedRedisConfigGroup() {
const groupId = document.getElementById('redisConfigGroupSelect').value;
if (!groupId) {
showAlert('请选择配置组', 'warning');
return;
}
try {
const response = await fetch(`/api/redis/config-groups/${groupId}`);
const result = await response.json();
if (result.success && result.data) {
const config = result.data;
// 加载集群1配置
loadClusterConfig('cluster1', config.cluster1_config);
// 加载集群2配置
loadClusterConfig('cluster2', config.cluster2_config);
// 加载查询选项
loadQueryOptions(config.query_options);
showAlert(`配置组 "${config.name}" 加载成功`, 'success');
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`加载失败: ${error.message}`, 'danger');
}
}
// 加载集群配置到界面
function loadClusterConfig(clusterId, config) {
// 设置集群名称
const nameElement = document.getElementById(`${clusterId}Name`);
if (nameElement && config.name) {
nameElement.value = config.name;
}
// 设置密码
const passwordElement = document.getElementById(`${clusterId}Password`);
if (passwordElement && config.password) {
passwordElement.value = config.password;
}
// 清空现有节点
const container = document.getElementById(`${clusterId}Nodes`);
if (container) {
container.innerHTML = '';
// 添加节点
if (config.nodes && config.nodes.length > 0) {
config.nodes.forEach(node => {
addRedisNode(clusterId);
const nodeInputs = container.querySelectorAll('.node-input');
const lastNodeInput = nodeInputs[nodeInputs.length - 1];
const hostInput = lastNodeInput.querySelector('input[type="text"]');
const portInput = lastNodeInput.querySelector('input[type="number"]');
if (hostInput) hostInput.value = node.host || '';
if (portInput) portInput.value = node.port || 6379;
});
} else {
// 添加默认节点
addRedisNode(clusterId);
}
}
}
// 加载查询选项
function loadQueryOptions(queryOptions) {
if (queryOptions.mode === 'random') {
document.getElementById('randomMode').checked = true;
document.getElementById('sampleCount').value = queryOptions.count || 100;
document.getElementById('keyPattern').value = queryOptions.pattern || '*';
document.getElementById('sourceCluster').value = queryOptions.source_cluster || 'cluster2';
} else {
document.getElementById('specifiedMode').checked = true;
document.getElementById('specifiedKeys').value = (queryOptions.keys || []).join('\n');
}
toggleQueryMode();
}
// 显示Redis配置管理对话框
function showManageRedisConfigDialog() {
loadRedisConfigGroupsForManagement();
new bootstrap.Modal(document.getElementById('manageRedisConfigModal')).show();
}
// 为管理界面加载Redis配置组
async function loadRedisConfigGroupsForManagement() {
try {
const response = await fetch('/api/redis/config-groups');
const result = await response.json();
const container = document.getElementById('redisConfigGroupList');
if (!container) {
console.error('Redis配置组列表容器未找到: redisConfigGroupList');
showAlert('Redis配置组列表容器未找到请检查页面结构', 'danger');
return;
}
if (result.success && result.data && result.data.length > 0) {
let html = '<div class="table-responsive"><table class="table table-striped">';
html += '<thead><tr><th>名称</th><th>描述</th><th>创建时间</th><th>操作</th></tr></thead><tbody>';
result.data.forEach(group => {
const safeName = (group.name || '').replace(/'/g, "\\'");
html += `
<tr>
<td>${group.name || '未命名'}</td>
<td>${group.description || '无描述'}</td>
<td>${new Date(group.created_at).toLocaleString('zh-CN')}</td>
<td>
<button class="btn btn-primary btn-sm me-1" onclick="loadRedisConfigGroupById(${group.id})">
<i class="fas fa-download"></i> 加载
</button>
<button class="btn btn-danger btn-sm" onclick="deleteRedisConfigGroup(${group.id}, '${safeName}')">
<i class="fas fa-trash"></i> 删除
</button>
</td>
</tr>
`;
});
html += '</tbody></table></div>';
container.innerHTML = html;
console.log(`成功加载 ${result.data.length} 个Redis配置组到管理界面`);
} else {
container.innerHTML = '<div class="text-center text-muted py-4">暂无Redis配置组</div>';
console.log('没有找到Redis配置组数据');
}
} catch (error) {
const container = document.getElementById('redisConfigGroupList');
if (container) {
container.innerHTML = '<div class="alert alert-danger">加载失败: ' + error.message + '</div>';
}
console.error('加载Redis配置组失败:', error);
showAlert('加载Redis配置组失败: ' + error.message, 'danger');
}
}
// 通过ID加载Redis配置组
async function loadRedisConfigGroupById(groupId) {
try {
const response = await fetch(`/api/redis/config-groups/${groupId}`);
const result = await response.json();
if (result.success && result.data) {
const config = result.data;
// 加载配置
loadClusterConfig('cluster1', config.cluster1_config);
loadClusterConfig('cluster2', config.cluster2_config);
loadQueryOptions(config.query_options);
// 关闭管理对话框
bootstrap.Modal.getInstance(document.getElementById('manageRedisConfigModal')).hide();
showAlert(`配置组 "${config.name}" 加载成功`, 'success');
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`加载失败: ${error.message}`, 'danger');
}
}
// 删除Redis配置组
async function deleteRedisConfigGroup(groupId, groupName) {
if (!confirm(`确定要删除配置组 "${groupName}" 吗?此操作不可恢复。`)) {
return;
}
try {
const response = await fetch(`/api/redis/config-groups/${groupId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
loadRedisConfigGroupsForManagement(); // 重新加载列表
loadRedisConfigGroups(); // 重新加载下拉框
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`删除失败: ${error.message}`, 'danger');
}
}
// 显示Redis配置导入对话框
function showImportRedisConfigDialog(targetCluster) {
currentImportTarget = targetCluster;
document.getElementById('redisConfigImportText').value = '';
new bootstrap.Modal(document.getElementById('importRedisConfigModal')).show();
}
// 导入Redis配置
async function importRedisConfig() {
const configText = document.getElementById('redisConfigImportText').value.trim();
if (!configText) {
showAlert('请输入配置内容', 'warning');
return;
}
try {
const response = await fetch('/api/redis/import-config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
config_text: configText
})
});
const result = await response.json();
if (result.success && result.data) {
// 将配置应用到目标集群
loadClusterConfig(currentImportTarget, result.data);
bootstrap.Modal.getInstance(document.getElementById('importRedisConfigModal')).hide();
showAlert(result.message, 'success');
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`导入失败: ${error.message}`, 'danger');
}
}
/**
* Redis查询历史管理功能
*/
// 加载Redis查询历史
async function loadRedisQueryHistory() {
try {
const response = await fetch('/api/redis/query-history');
const result = await response.json();
const select = document.getElementById('redisHistorySelect');
if (!select) {
console.error('未找到Redis查询历史下拉框元素: redisHistorySelect');
return;
}
select.innerHTML = '<option value="">选择历史记录...</option>';
if (result.success && result.data) {
result.data.forEach(history => {
const option = document.createElement('option');
option.value = history.id;
option.textContent = `${history.name} - ${new Date(history.created_at).toLocaleString('zh-CN')}`;
select.appendChild(option);
});
console.log(`成功加载 ${result.data.length} 个Redis查询历史记录`);
} else {
console.log('没有找到Redis查询历史数据');
}
} catch (error) {
console.error('加载Redis查询历史失败:', error);
showAlert('加载Redis查询历史失败: ' + error.message, 'danger');
}
}
// 显示保存Redis查询历史对话框
function showSaveRedisHistoryDialog() {
if (!currentResults) {
showAlert('请先执行Redis比较查询', 'warning');
return;
}
// 生成默认名称
const timestamp = new Date().toLocaleString('zh-CN');
document.getElementById('redisHistoryName').value = `Redis查询_${timestamp}`;
document.getElementById('redisHistoryDescription').value = `Redis比较结果 - 总计${currentResults.stats.total_keys}个Key发现${currentResults.stats.different_count}处差异`;
new bootstrap.Modal(document.getElementById('saveRedisHistoryModal')).show();
}
// 保存Redis查询历史
async function saveRedisQueryHistory() {
if (!currentResults) {
showAlert('没有可保存的查询结果', 'warning');
return;
}
const name = document.getElementById('redisHistoryName').value.trim();
const description = document.getElementById('redisHistoryDescription').value.trim();
if (!name) {
showAlert('请输入历史记录名称', 'warning');
return;
}
try {
const response = await fetch('/api/redis/query-history', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
description: description,
cluster1_config: getClusterConfig('cluster1'),
cluster2_config: getClusterConfig('cluster2'),
query_options: getQueryOptions(),
query_keys: currentResults.query_options?.keys || [],
results_summary: currentResults.stats,
execution_time: currentResults.performance_report?.total_time || 0,
total_keys: currentResults.stats.total_keys,
different_count: currentResults.stats.different_count,
identical_count: currentResults.stats.identical_count,
missing_count: currentResults.stats.missing_in_cluster1 + currentResults.stats.missing_in_cluster2,
raw_results: {
identical_results: currentResults.identical_results,
different_results: currentResults.different_results,
missing_results: currentResults.missing_results,
performance_report: currentResults.performance_report
}
})
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
bootstrap.Modal.getInstance(document.getElementById('saveRedisHistoryModal')).hide();
loadRedisQueryHistory(); // 重新加载历史记录列表
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`保存失败: ${error.message}`, 'danger');
}
}
// 加载选定的Redis查询历史
async function loadSelectedRedisHistory() {
const historyId = document.getElementById('redisHistorySelect').value;
if (!historyId) {
showAlert('请选择历史记录', 'warning');
return;
}
try {
const response = await fetch(`/api/redis/query-history/${historyId}`);
const result = await response.json();
if (result.success && result.data) {
const history = result.data;
// 加载配置
loadClusterConfig('cluster1', history.cluster1_config);
loadClusterConfig('cluster2', history.cluster2_config);
loadQueryOptions(history.query_options);
// 如果有原始结果,直接显示
if (history.raw_results) {
const resultsData = {
stats: history.results_summary,
identical_results: history.raw_results.identical_results || [],
different_results: history.raw_results.different_results || [],
missing_results: history.raw_results.missing_results || [],
performance_report: history.raw_results.performance_report || {},
clusters: {
cluster1_name: history.cluster1_config.name,
cluster2_name: history.cluster2_config.name
}
};
currentResults = resultsData;
displayResults(resultsData);
showAlert(`历史记录 "${history.name}" 加载成功`, 'success');
} else {
showAlert(`历史记录 "${history.name}" 配置加载成功,但没有结果数据`, 'info');
}
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`加载失败: ${error.message}`, 'danger');
}
}
// 显示Redis查询历史管理对话框
function showManageRedisHistoryDialog() {
loadRedisHistoryForManagement();
new bootstrap.Modal(document.getElementById('manageRedisHistoryModal')).show();
}
// 为管理界面加载Redis查询历史
async function loadRedisHistoryForManagement() {
try {
const response = await fetch('/api/redis/query-history');
const result = await response.json();
const container = document.getElementById('redisHistoryList');
if (!container) {
console.error('历史记录列表容器未找到: redisHistoryList');
showAlert('历史记录列表容器未找到,请检查页面结构', 'danger');
return;
}
if (result.success && result.data && result.data.length > 0) {
let html = '<div class="table-responsive"><table class="table table-striped">';
html += '<thead><tr><th>名称</th><th>描述</th><th>Key数量</th><th>差异数</th><th>执行时间</th><th>创建时间</th><th>操作</th></tr></thead><tbody>';
result.data.forEach(history => {
const safeName = (history.name || '').replace(/'/g, "\\'");
html += `
<tr>
<td>${history.name || '未命名'}</td>
<td>${history.description || '无描述'}</td>
<td>${history.total_keys || 0}</td>
<td>${history.different_count || 0}</td>
<td>${(history.execution_time || 0).toFixed(3)}s</td>
<td>${new Date(history.created_at).toLocaleString('zh-CN')}</td>
<td>
<button class="btn btn-warning btn-sm me-1" onclick="loadRedisHistoryById(${history.id})">
<i class="fas fa-history"></i> 加载
</button>
<button class="btn btn-danger btn-sm" onclick="deleteRedisHistory(${history.id}, '${safeName}')">
<i class="fas fa-trash"></i> 删除
</button>
</td>
</tr>
`;
});
html += '</tbody></table></div>';
container.innerHTML = html;
console.log(`成功加载 ${result.data.length} 个Redis历史记录到管理界面`);
} else {
container.innerHTML = '<div class="text-center text-muted py-4">暂无Redis查询历史</div>';
console.log('没有找到Redis历史记录数据');
}
} catch (error) {
const container = document.getElementById('redisHistoryList');
if (container) {
container.innerHTML = '<div class="alert alert-danger">加载失败: ' + error.message + '</div>';
}
console.error('加载Redis历史记录失败:', error);
showAlert('加载Redis历史记录失败: ' + error.message, 'danger');
}
}
// 通过ID加载Redis查询历史
async function loadRedisHistoryById(historyId) {
try {
const response = await fetch(`/api/redis/query-history/${historyId}`);
const result = await response.json();
if (result.success && result.data) {
const history = result.data;
// 加载配置
loadClusterConfig('cluster1', history.cluster1_config);
loadClusterConfig('cluster2', history.cluster2_config);
loadQueryOptions(history.query_options);
// 如果有原始结果,直接显示
if (history.raw_results) {
const resultsData = {
stats: history.results_summary,
identical_results: history.raw_results.identical_results || [],
different_results: history.raw_results.different_results || [],
missing_results: history.raw_results.missing_results || [],
performance_report: history.raw_results.performance_report || {},
clusters: {
cluster1_name: history.cluster1_config.name,
cluster2_name: history.cluster2_config.name
}
};
currentResults = resultsData;
displayResults(resultsData);
}
// 关闭管理对话框
bootstrap.Modal.getInstance(document.getElementById('manageRedisHistoryModal')).hide();
showAlert(`历史记录 "${history.name}" 加载成功`, 'success');
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`加载失败: ${error.message}`, 'danger');
}
}
// 删除Redis查询历史
async function deleteRedisHistory(historyId, historyName) {
if (!confirm(`确定要删除历史记录 "${historyName}" 吗?此操作不可恢复。`)) {
return;
}
try {
const response = await fetch(`/api/redis/query-history/${historyId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
loadRedisHistoryForManagement(); // 重新加载列表
loadRedisQueryHistory(); // 重新加载下拉框
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`删除失败: ${error.message}`, 'danger');
}
}
/**
* Redis查询日志功能
*/
// 显示Redis查询日志对话框
function showRedisQueryLogsDialog() {
loadRedisQueryLogs();
new bootstrap.Modal(document.getElementById('redisQueryLogsModal')).show();
}
// 加载Redis查询日志
async function loadRedisQueryLogs() {
try {
const response = await fetch('/api/redis/query-logs?limit=1000');
const result = await response.json();
const container = document.getElementById('redisQueryLogs');
if (result.success && result.data && result.data.length > 0) {
let html = '';
result.data.forEach(log => {
const levelClass = log.level === 'ERROR' ? 'text-danger' :
log.level === 'WARNING' ? 'text-warning' : 'text-info';
const timestamp = log.timestamp || '未知时间';
const level = log.level || 'INFO';
const message = log.message || '无消息内容';
html += `
<div class="mb-2">
<span class="text-muted">[${timestamp}]</span>
<span class="badge bg-secondary">${level}</span>
<span class="${levelClass}">${message}</span>
</div>
`;
});
container.innerHTML = html;
} else {
container.innerHTML = '<div class="text-center text-muted py-4">暂无Redis查询日志</div>';
}
} catch (error) {
document.getElementById('redisQueryLogs').innerHTML = '<div class="alert alert-danger">加载日志失败: ' + error.message + '</div>';
}
}
// 刷新Redis查询日志
function refreshRedisQueryLogs() {
loadRedisQueryLogs();
showAlert('查询日志已刷新', 'info');
}
// 清空Redis查询日志
async function clearRedisQueryLogs() {
if (!confirm('确定要清空所有查询日志吗?此操作不可恢复。')) {
return;
}
try {
const response = await fetch('/api/redis/query-logs', {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
loadRedisQueryLogs(); // 重新加载日志
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`清空日志失败: ${error.message}`, 'danger');
}
}
// 显示导入Redis配置对话框
function showImportRedisConfigDialog() {
updateConfigTemplate();
new bootstrap.Modal(document.getElementById('importRedisConfigModal')).show();
}
// 更新配置模板
function updateConfigTemplate() {
const format = document.getElementById('configFormat').value;
const templateElement = document.getElementById('configTemplate');
if (format === 'yaml') {
templateElement.textContent = `# Redis集群配置示例 (YAML格式)
cluster1:
clusterName: "redis-production"
clusterAddress: "10.20.2.109:6470"
clusterPassword: ""
cachePrefix: "message.status.Reader."
cacheTtl: 2000
async: true
nodes:
- host: "10.20.2.109"
port: 6470
- host: "10.20.2.110"
port: 6470
cluster2:
clusterName: "redis-test"
clusterAddress: "10.20.2.109:6471"
clusterPassword: ""
cachePrefix: "message.status.Reader."
cacheTtl: 2000
async: true
nodes:
- host: "10.20.2.109"
port: 6471
- host: "10.20.2.110"
port: 6471
queryOptions:
mode: "random" # random 或 specified
count: 100 # 随机采样数量
pattern: "*" # Key匹配模式
sourceCluster: "cluster2"
# 指定Key模式下的键值列表
keys:
- "user:1001"
- "user:1002"`;
} else {
templateElement.textContent = `{
"cluster1": {
"clusterName": "redis-production",
"clusterAddress": "10.20.2.109:6470",
"clusterPassword": "",
"cachePrefix": "message.status.Reader.",
"cacheTtl": 2000,
"async": true,
"nodes": [
{"host": "10.20.2.109", "port": 6470},
{"host": "10.20.2.110", "port": 6470}
]
},
"cluster2": {
"clusterName": "redis-test",
"clusterAddress": "10.20.2.109:6471",
"clusterPassword": "",
"cachePrefix": "message.status.Reader.",
"cacheTtl": 2000,
"async": true,
"nodes": [
{"host": "10.20.2.109", "port": 6471},
{"host": "10.20.2.110", "port": 6471}
]
},
"queryOptions": {
"mode": "random",
"count": 100,
"pattern": "*",
"sourceCluster": "cluster2",
"keys": ["user:1001", "user:1002"]
}
}`;
}
}
// 导入Redis配置
async function importRedisConfig() {
const format = document.getElementById('configFormat').value;
const content = document.getElementById('configContent').value.trim();
if (!content) {
showAlert('请输入配置内容', 'warning');
return;
}
try {
let config;
if (format === 'yaml') {
// 解析YAML格式简单解析不使用第三方库
config = parseSimpleYaml(content);
} else {
// 解析JSON格式
config = JSON.parse(content);
}
// 验证配置结构
if (!config.cluster1 || !config.cluster2) {
showAlert('配置格式错误缺少cluster1或cluster2配置', 'danger');
return;
}
// 应用配置到页面
if (config.cluster1) {
loadClusterConfig('cluster1', config.cluster1);
}
if (config.cluster2) {
loadClusterConfig('cluster2', config.cluster2);
}
if (config.queryOptions) {
loadQueryOptions(config.queryOptions);
}
// 关闭对话框
bootstrap.Modal.getInstance(document.getElementById('importRedisConfigModal')).hide();
showAlert('配置导入成功!', 'success');
} catch (error) {
showAlert(`配置解析失败: ${error.message}`, 'danger');
}
}
// 简单的YAML解析器仅支持基本格式
function parseSimpleYaml(yamlContent) {
const lines = yamlContent.split('\n');
const result = {};
let currentSection = null;
let currentSubSection = null;
for (let line of lines) {
line = line.trim();
// 跳过注释和空行
if (!line || line.startsWith('#')) continue;
// 检查是否是主节点
if (line.match(/^[a-zA-Z][a-zA-Z0-9_]*:$/)) {
currentSection = line.slice(0, -1);
result[currentSection] = {};
currentSubSection = null;
}
// 检查是否是子节点
else if (line.match(/^[a-zA-Z][a-zA-Z0-9_]*:/) && currentSection) {
const [key, ...valueParts] = line.split(':');
let value = valueParts.join(':').trim();
// 处理字符串值
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
} else if (value === 'true') {
value = true;
} else if (value === 'false') {
value = false;
} else if (!isNaN(value) && value !== '') {
value = Number(value);
}
if (key === 'nodes') {
result[currentSection][key] = [];
currentSubSection = key;
} else {
result[currentSection][key] = value;
}
}
// 处理数组项
else if (line.startsWith('- ') && currentSubSection === 'nodes') {
const nodeStr = line.substring(2).trim();
if (nodeStr.includes('host:') && nodeStr.includes('port:')) {
// 解析 "host: xxx, port: xxx" 格式
const hostMatch = nodeStr.match(/host:\s*"?([^",]+)"?/);
const portMatch = nodeStr.match(/port:\s*(\d+)/);
if (hostMatch && portMatch) {
result[currentSection][currentSubSection].push({
host: hostMatch[1],
port: parseInt(portMatch[1])
});
}
}
}
}
return result;
}
// 添加Redis节点
function addRedisNode(clusterId) {
const nodeContainer = document.getElementById(`${clusterId}Nodes`);
const nodeDiv = document.createElement('div');
nodeDiv.className = 'node-input';
nodeDiv.innerHTML = `
<input type="text" class="form-control form-control-sm me-2" placeholder="127.0.0.1" value="127.0.0.1" style="flex: 2;">
<input type="number" class="form-control form-control-sm me-2" placeholder="6379" value="6379" style="flex: 1;">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeRedisNode(this, '${clusterId}')">
<i class="fas fa-minus"></i>
</button>
`;
nodeContainer.appendChild(nodeDiv);
}
// 删除Redis节点
function removeRedisNode(button, clusterId) {
const nodeContainer = document.getElementById(`${clusterId}Nodes`);
const nodeInputs = nodeContainer.querySelectorAll('.node-input');
// 至少保留一个节点
if (nodeInputs.length <= 1) {
showAlert('至少需要保留一个Redis节点', 'warning');
return;
}
// 删除当前节点
button.parentElement.remove();
}
// 切换查询模式
function toggleQueryMode() {
const randomMode = document.getElementById('randomMode');
const randomOptions = document.getElementById('randomOptions');
const specifiedOptions = document.getElementById('specifiedOptions');
if (randomMode.checked) {
randomOptions.style.display = 'block';
specifiedOptions.style.display = 'none';
} else {
randomOptions.style.display = 'none';
specifiedOptions.style.display = 'block';
}
}
/**
* 原生数据展示功能
*/
// 全局变量存储原始数据和当前视图状态
let rawDataState = {
cluster1: { data: null, view: 'formatted' },
cluster2: { data: null, view: 'formatted' }
};
/**
* 显示原生数据
*/
function displayRawData(results) {
console.log('开始显示原生数据');
// 从结果中收集所有键值数据
const cluster1Data = collectClusterData(results, 'cluster1');
const cluster2Data = collectClusterData(results, 'cluster2');
// 存储原始数据
rawDataState.cluster1.data = cluster1Data;
rawDataState.cluster2.data = cluster2Data;
// 显示数据
renderRawData('cluster1', cluster1Data, 'formatted');
renderRawData('cluster2', cluster2Data, 'formatted');
console.log(`原生数据显示完成 - 集群1: ${cluster1Data.length}条, 集群2: ${cluster2Data.length}`);
}
/**
* 从结果中收集集群数据
*/
function collectClusterData(results, clusterType) {
const data = [];
const clusterField = clusterType === 'cluster1' ? 'cluster1_value' : 'cluster2_value';
// 从相同结果中收集数据
if (results.identical_results) {
results.identical_results.forEach(item => {
if (item.key && item.value !== undefined) {
data.push({
key: item.key,
value: item.value,
type: 'identical'
});
}
});
}
// 从差异结果中收集数据
if (results.different_results) {
results.different_results.forEach(item => {
if (item.key && item[clusterField] !== undefined) {
data.push({
key: item.key,
value: item[clusterField],
type: 'different'
});
}
});
}
// 从缺失结果中收集数据
if (results.missing_results) {
results.missing_results.forEach(item => {
if (item.key && item[clusterField] !== undefined) {
data.push({
key: item.key,
value: item[clusterField],
type: 'missing'
});
}
});
}
return data;
}
/**
* 渲染原生数据
*/
function renderRawData(clusterId, data, viewType) {
const container = document.getElementById(`${clusterId}-raw-data`);
if (!container) {
console.error(`原生数据容器未找到: ${clusterId}-raw-data`);
return;
}
if (!data || data.length === 0) {
container.innerHTML = '<div class="text-muted text-center py-4">无数据</div>';
return;
}
let html = '';
if (viewType === 'formatted') {
// 格式化视图
html = '<div class="raw-data-formatted">';
data.forEach((item, index) => {
const typeClass = getTypeClass(item.type);
const formattedValue = formatRedisValue(item.value);
html += `
<div class="raw-data-item mb-3 p-3 border rounded ${typeClass}">
<div class="d-flex justify-content-between align-items-center mb-2">
<strong class="text-primary">Key: ${escapeHtml(item.key)}</strong>
<span class="badge bg-secondary">${item.type}</span>
</div>
<div class="raw-data-value">
<pre class="redis-value mb-0">${escapeHtml(formattedValue)}</pre>
</div>
</div>
`;
});
html += '</div>';
} else {
// 原始视图
html = '<div class="raw-data-raw">';
html += '<pre class="redis-value">';
data.forEach((item, index) => {
html += `Key: ${escapeHtml(item.key)}\n`;
html += `Type: ${item.type}\n`;
html += `Value: ${escapeHtml(String(item.value))}\n`;
if (index < data.length - 1) {
html += '\n' + '='.repeat(50) + '\n\n';
}
});
html += '</pre>';
html += '</div>';
}
container.innerHTML = html;
}
/**
* 获取类型对应的CSS类
*/
function getTypeClass(type) {
switch (type) {
case 'identical': return 'border-success';
case 'different': return 'border-warning';
case 'missing': return 'border-danger';
default: return 'border-secondary';
}
}
/**
* HTML转义
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* 切换原生数据视图
*/
function switchRawDataView(clusterId, viewType) {
if (!rawDataState[clusterId] || !rawDataState[clusterId].data) {
console.error(`没有${clusterId}的原生数据`);
return;
}
// 更新按钮状态
const container = document.getElementById(`${clusterId}-raw-data`);
const buttonGroup = container.parentElement.querySelector('.btn-group');
if (buttonGroup) {
const buttons = buttonGroup.querySelectorAll('button');
buttons.forEach(btn => {
btn.classList.remove('active');
const btnText = btn.textContent.trim();
if ((viewType === 'formatted' && btnText === '格式化') ||
(viewType === 'raw' && btnText === '原始')) {
btn.classList.add('active');
}
});
}
// 更新视图状态
rawDataState[clusterId].view = viewType;
// 重新渲染数据
renderRawData(clusterId, rawDataState[clusterId].data, viewType);
console.log(`${clusterId}视图已切换到: ${viewType}`);
}
/**
* 导出原生数据
*/
function exportRawData(clusterId) {
if (!rawDataState[clusterId] || !rawDataState[clusterId].data) {
showAlert(`没有${clusterId}的原生数据可导出`, 'warning');
return;
}
const data = rawDataState[clusterId].data;
const clusterName = clusterId === 'cluster1' ?
(currentResults?.clusters?.cluster1_name || '集群1') :
(currentResults?.clusters?.cluster2_name || '集群2');
// 生成导出内容
let content = `Redis原生数据导出 - ${clusterName}\n`;
content += `导出时间: ${new Date().toLocaleString('zh-CN')}\n`;
content += `数据总数: ${data.length}\n`;
content += '='.repeat(60) + '\n\n';
data.forEach((item, index) => {
content += `[${index + 1}] Key: ${item.key}\n`;
content += `Type: ${item.type}\n`;
content += `Value:\n${item.value}\n`;
content += '\n' + '-'.repeat(40) + '\n\n';
});
// 创建下载
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `redis_raw_data_${clusterId}_${new Date().getTime()}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showAlert(`${clusterName}原生数据导出成功`, 'success');
}