Files
BigDataTool/static/js/redis_compare.js
2025-08-12 16:27:00 +08:00

3241 lines
110 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 });
}
});
// 获取连接参数
const socketTimeout = parseInt(document.getElementById(`${clusterId}SocketTimeout`).value) || 3;
const socketConnectTimeout = parseInt(document.getElementById(`${clusterId}SocketConnectTimeout`).value) || 3;
const maxConnectionsPerNode = parseInt(document.getElementById(`${clusterId}MaxConnectionsPerNode`).value) || 16;
return {
name,
nodes,
password: password || null,
socket_timeout: socketTimeout,
socket_connect_timeout: socketConnectTimeout,
max_connections_per_node: maxConnectionsPerNode
};
}
/**
* 获取查询选项
*/
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) => {
const cluster1Type = diff.cluster1_type ? ` (${diff.cluster1_type})` : '';
const cluster2Type = diff.cluster2_type ? ` (${diff.cluster2_type})` : '';
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'}${cluster1Type}:</strong>
<pre class="redis-value mt-2">${formatRedisValue(diff.cluster1_value)}</pre>
</div>
<div class="col-md-6">
<strong>${currentResults.clusters.cluster2_name || '集群2'}${cluster2Type}:</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) => {
const typeInfo = item.type ? ` (${item.type})` : '';
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>值${typeInfo}:</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 && item.cluster1_value !== null ? `
<div class="mt-2">
<strong>${currentResults.clusters.cluster1_name || '集群1'}${item.cluster1_type ? ` (${item.cluster1_type})` : ''}:</strong>
<pre class="redis-value mt-1">${formatRedisValue(item.cluster1_value)}</pre>
</div>
` : ''}
${item.cluster2_value !== undefined && item.cluster2_value !== null ? `
<div class="mt-2">
<strong>${currentResults.clusters.cluster2_name || '集群2'}${item.cluster2_type ? ` (${item.cluster2_type})` : ''}:</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 showRedisQueryHistoryDialog() {
loadRedisHistoryForManagement();
new bootstrap.Modal(document.getElementById('redisQueryHistoryModal')).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="d-flex justify-content-between align-items-center mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAllRedisHistory" onchange="toggleAllRedisHistorySelection()">
<label class="form-check-label" for="selectAllRedisHistory">
全选
</label>
</div>
<div>
<button class="btn btn-danger btn-sm" id="batchDeleteRedisHistoryBtn" onclick="batchDeleteRedisHistory()" disabled>
<i class="fas fa-trash"></i> 批量删除 (<span id="selectedRedisHistoryCount">0</span>)
</button>
</div>
</div>
`;
html += '<div class="table-responsive"><table class="table table-striped">';
html += '<thead><tr><th width="40">选择</th><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>
<div class="form-check">
<input class="form-check-input redis-history-checkbox" type="checkbox" value="${history.id}" onchange="updateRedisHistorySelectionCount()">
</div>
</td>
<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&grouped=true');
const result = await response.json();
const container = document.getElementById('redis-modal-query-logs');
if (result.success && result.data && result.data.length > 0) {
displayRedisGroupedLogs(result.data, container);
} else {
container.innerHTML = '<div class="text-center text-muted py-4">暂无Redis查询日志</div>';
}
} catch (error) {
document.getElementById('redis-modal-query-logs').innerHTML = '<div class="alert alert-danger">加载日志失败: ' + error.message + '</div>';
}
}
// 显示分组的Redis查询日志
function displayRedisGroupedLogs(groupedLogs, container) {
let html = '';
// 添加全部展开/折叠控制按钮
html += `
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Redis查询日志 (共 ${groupedLogs.length} 个批次)</h6>
<div>
<button class="btn btn-sm btn-outline-primary me-2" onclick="expandAllRedisLogGroups()">
<i class="fas fa-expand-arrows-alt"></i> 全部展开
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="collapseAllRedisLogGroups()">
<i class="fas fa-compress-arrows-alt"></i> 全部折叠
</button>
</div>
</div>
`;
// 按时间倒序显示日志组(最新的在前面)
groupedLogs.reverse().forEach((group, index) => {
const [batchId, logs] = group;
const isFirstGroup = index === 0; // 第一个(最新的)组默认展开
// 分析批次信息
const batchInfo = analyzeBatchInfo(logs);
const statusIcon = getStatusIcon(batchInfo.status);
const statusClass = getStatusClass(batchInfo.status);
html += `
<div class="card mb-3 redis-log-group">
<div class="card-header ${statusClass}" style="cursor: pointer;" onclick="toggleRedisLogGroup('${batchId}')">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">
${statusIcon} 批次: ${batchId}
<small class="text-muted ms-2">${batchInfo.startTime}</small>
</h6>
<small class="text-muted">
${batchInfo.totalKeys} 个Key | ${batchInfo.duration} | ${batchInfo.summary}
</small>
</div>
<div>
<span class="badge bg-secondary">${logs.length} 条日志</span>
<i class="fas fa-chevron-${isFirstGroup ? 'up' : 'down'} ms-2" id="chevron-${batchId}"></i>
</div>
</div>
</div>
<div class="card-body ${isFirstGroup ? '' : 'd-none'}" id="logs-${batchId}">
<div class="redis-log-content">
${generateLogItemsHtml(logs)}
</div>
</div>
</div>
`;
});
container.innerHTML = html;
}
// 生成日志项的HTML
function generateLogItemsHtml(logs) {
let html = '';
logs.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 || '无消息内容';
// 提取时间部分(只显示时分秒)
const timeOnly = timestamp.split(' ')[1] || timestamp;
html += `
<div class="log-item mb-1 p-2 border-start border-3 ${getBorderClass(level)}" data-level="${level}">
<div class="d-flex align-items-start">
<span class="badge ${getBadgeClass(level)} me-2">${level}</span>
<div class="flex-grow-1">
<div class="${levelClass}">${message}</div>
<small class="text-muted">${timeOnly}</small>
</div>
</div>
</div>
`;
});
return html;
}
// 分析批次信息
function analyzeBatchInfo(logs) {
const firstLog = logs[0];
const lastLog = logs[logs.length - 1];
// 提取开始时间
const startTime = firstLog.timestamp || '未知时间';
// 计算持续时间
let duration = '未知';
if (firstLog.timestamp && lastLog.timestamp) {
const start = new Date(firstLog.timestamp);
const end = new Date(lastLog.timestamp);
const diffMs = end - start;
if (diffMs >= 0) {
duration = `${(diffMs / 1000).toFixed(2)}`;
}
}
// 分析状态
let status = 'success';
let totalKeys = 0;
let summary = '';
const hasError = logs.some(log => log.level === 'ERROR');
const hasWarning = logs.some(log => log.level === 'WARNING');
if (hasError) {
status = 'error';
} else if (hasWarning) {
status = 'warning';
}
// 提取Key数量和总结信息
logs.forEach(log => {
const message = log.message || '';
// 提取Key数量
const keyMatch = message.match(/(\d+)\s*个[Kk]ey/);
if (keyMatch) {
totalKeys = Math.max(totalKeys, parseInt(keyMatch[1]));
}
// 提取总结信息
if (message.includes('比对统计总览') || message.includes('数据一致') || message.includes('数据不同')) {
const summaryMatch = message.match(/数据一致:\s*(\d+).*数据不同:\s*(\d+)/);
if (summaryMatch) {
summary = `一致${summaryMatch[1]}个, 不同${summaryMatch[2]}`;
}
}
});
if (!summary) {
summary = `${logs.length}条日志`;
}
return {
startTime,
duration,
status,
totalKeys: totalKeys || '未知',
summary
};
}
// 获取状态图标
function getStatusIcon(status) {
switch (status) {
case 'success':
return '<i class="fas fa-check-circle text-success"></i>';
case 'warning':
return '<i class="fas fa-exclamation-triangle text-warning"></i>';
case 'error':
return '<i class="fas fa-times-circle text-danger"></i>';
default:
return '<i class="fas fa-info-circle text-info"></i>';
}
}
// 获取状态样式类
function getStatusClass(status) {
switch (status) {
case 'success':
return 'bg-light border-success';
case 'warning':
return 'bg-warning bg-opacity-10 border-warning';
case 'error':
return 'bg-danger bg-opacity-10 border-danger';
default:
return 'bg-light';
}
}
// 获取边框样式类
function getBorderClass(level) {
switch (level) {
case 'ERROR':
return 'border-danger';
case 'WARNING':
return 'border-warning';
case 'INFO':
return 'border-info';
default:
return 'border-secondary';
}
}
// 获取徽章样式类
function getBadgeClass(level) {
switch (level) {
case 'ERROR':
return 'bg-danger';
case 'WARNING':
return 'bg-warning text-dark';
case 'INFO':
return 'bg-info';
default:
return 'bg-secondary';
}
}
// 切换日志组的展开/折叠状态
function toggleRedisLogGroup(batchId) {
const logsContainer = document.getElementById(`logs-${batchId}`);
const chevron = document.getElementById(`chevron-${batchId}`);
if (logsContainer.classList.contains('d-none')) {
logsContainer.classList.remove('d-none');
chevron.className = 'fas fa-chevron-up ms-2';
} else {
logsContainer.classList.add('d-none');
chevron.className = 'fas fa-chevron-down ms-2';
}
}
// 展开所有日志组
function expandAllRedisLogGroups() {
document.querySelectorAll('.redis-log-group .card-body').forEach(container => {
container.classList.remove('d-none');
});
document.querySelectorAll('[id^="chevron-"]').forEach(chevron => {
chevron.className = 'fas fa-chevron-up ms-2';
});
}
// 折叠所有日志组
function collapseAllRedisLogGroups() {
document.querySelectorAll('.redis-log-group .card-body').forEach(container => {
container.classList.add('d-none');
});
document.querySelectorAll('[id^="chevron-"]').forEach(chevron => {
chevron.className = 'fas fa-chevron-down ms-2';
});
}
// 刷新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-example1"
clusterAddress: "127.0.0.1:6379"
clusterPassword: ""
cachePrefix: "message.status.Reader."
cacheTtl: 2000
async: true
nodes:
- host: "127.0.0.1"
port: 6379
- host: "127.0.0.2"
port: 6379
cluster2:
clusterName: "redis-example2"
clusterAddress: "127.0.0.1:6380"
clusterPassword: ""
cachePrefix: "message.status.Reader."
cacheTtl: 2000
async: true
nodes:
- host: "127.0.0.1"
port: 6380
- host: "127.0.0.2"
port: 6380
queryOptions:
mode: "random" # random 或 specified
count: 100 # 随机采样数量
pattern: "*" # Key匹配模式
sourceCluster: "cluster2"
# 指定Key模式下的键值列表
keys:
- "user:example1"
- "user:example2"`;
} else {
templateElement.textContent = `{
"cluster1": {
"clusterName": "redis-example1",
"clusterAddress": "127.0.0.1:6379",
"clusterPassword": "",
"cachePrefix": "message.status.Reader.",
"cacheTtl": 2000,
"async": true,
"nodes": [
{"host": "127.0.0.1", "port": 6379},
{"host": "127.0.0.2", "port": 6379}
]
},
"cluster2": {
"clusterName": "redis-example2",
"clusterAddress": "127.0.0.1:6380",
"clusterPassword": "",
"cachePrefix": "message.status.Reader.",
"cacheTtl": 2000,
"async": true,
"nodes": [
{"host": "127.0.0.1", "port": 6380},
{"host": "127.0.0.2", "port": 6380}
]
},
"queryOptions": {
"mode": "random",
"count": 100,
"pattern": "*",
"sourceCluster": "cluster2",
"keys": ["user:example1", "user:example2"]
}
}`;
}
}
// 导入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';
const clusterTypeField = clusterType === 'cluster1' ? 'cluster1_type' : 'cluster2_type';
// 从相同结果中收集数据
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',
redis_type: item.type || 'unknown'
});
}
});
}
// 从差异结果中收集数据
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',
redis_type: item[clusterTypeField] || 'unknown'
});
}
});
}
// 从缺失结果中收集数据
if (results.missing_results) {
results.missing_results.forEach(item => {
if (item.key && item[clusterField] !== undefined && item[clusterField] !== null) {
data.push({
key: item.key,
value: item[clusterField],
type: 'missing',
redis_type: item[clusterTypeField] || 'unknown'
});
}
});
}
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);
const redisTypeInfo = item.redis_type ? ` [${item.redis_type}]` : '';
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)}${redisTypeInfo}</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 += `Redis Type: ${item.redis_type || 'unknown'}\n`;
html += `Comparison 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');
}
/**
* Redis配置组管理功能
*/
// 显示保存Redis配置组对话框
function showSaveRedisConfigDialog() {
// 清空表单
document.getElementById('redisConfigName').value = '';
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;
}
try {
// 获取当前配置
const cluster1Config = getClusterConfig('cluster1');
const cluster2Config = getClusterConfig('cluster2');
const queryOptions = getQueryOptions();
const configData = {
name: name,
description: description,
cluster1_config: cluster1Config,
cluster2_config: cluster2Config,
query_options: queryOptions
};
const response = await fetch('/api/redis/config-groups', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(configData)
});
const result = await response.json();
if (result.success) {
showAlert('Redis配置组保存成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('saveRedisConfigModal')).hide();
loadRedisConfigGroups(); // 刷新配置组列表
} else {
showAlert(`保存失败: ${result.error}`, 'danger');
}
} catch (error) {
console.error('保存Redis配置组失败:', error);
showAlert(`保存失败: ${error.message}`, 'danger');
}
}
// 显示管理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();
if (result.success) {
displayRedisConfigGroupsForManagement(result.data);
} else {
showAlert(`加载配置组失败: ${result.error}`, 'danger');
}
} catch (error) {
console.error('加载Redis配置组失败:', error);
showAlert(`加载失败: ${error.message}`, 'danger');
}
}
// 显示Redis配置组管理列表
function displayRedisConfigGroupsForManagement(configGroups) {
const container = document.getElementById('redisConfigGroupsList');
if (!configGroups || configGroups.length === 0) {
container.innerHTML = '<div class="text-center text-muted py-4">暂无保存的配置组</div>';
return;
}
let html = '';
configGroups.forEach(config => {
html += `
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="card-title mb-2">
<i class="fas fa-cog redis-logo me-2"></i>${config.name}
</h6>
<p class="card-text text-muted small mb-2">${config.description || '无描述'}</p>
<div class="small text-muted">
<i class="fas fa-clock me-1"></i>创建时间: ${config.created_at}
${config.updated_at !== config.created_at ? `<br><i class="fas fa-edit me-1"></i>更新时间: ${config.updated_at}` : ''}
</div>
</div>
<div class="btn-group-vertical btn-group-sm">
<button class="btn btn-outline-primary btn-sm mb-1" onclick="loadRedisConfigGroup(${config.id})" title="加载配置">
<i class="fas fa-download"></i> 加载
</button>
<button class="btn btn-outline-success btn-sm mb-1" onclick="exportRedisConfigGroup(${config.id}, '${config.name}')" title="导出YAML">
<i class="fas fa-file-export"></i> 导出
</button>
<button class="btn btn-outline-info btn-sm mb-1" onclick="copyRedisConfigGroup(${config.id}, '${config.name}')" title="复制配置">
<i class="fas fa-copy"></i> 复制
</button>
<button class="btn btn-outline-warning btn-sm mb-1" onclick="testRedisConfigConnection(${config.id})" title="测试连接">
<i class="fas fa-plug"></i> 测试
</button>
<button class="btn btn-outline-danger btn-sm" onclick="deleteRedisConfigGroup(${config.id}, '${config.name}')" title="删除配置">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
}
// 刷新Redis配置组列表
function refreshRedisConfigGroups() {
loadRedisConfigGroupsForManagement();
}
/**
* Redis查询历史保存功能
*/
// 显示保存Redis查询历史对话框
function showSaveRedisHistoryDialog() {
if (!currentResults) {
showAlert('请先执行查询后再保存历史记录', 'warning');
return;
}
// 生成默认名称
const now = new Date();
const defaultName = `Redis查询_${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`;
document.getElementById('redisHistoryName').value = defaultName;
// 生成默认描述
const stats = currentResults.stats || {};
const defaultDescription = `自动保存 - 查询${stats.total_keys || 0}个Key发现${stats.different_count || 0}处差异`;
document.getElementById('redisHistoryDescription').value = defaultDescription;
// 显示模态框
new bootstrap.Modal(document.getElementById('saveRedisHistoryModal')).show();
}
// 保存Redis查询历史
async function saveRedisQueryHistory() {
const name = document.getElementById('redisHistoryName').value.trim();
const description = document.getElementById('redisHistoryDescription').value.trim();
if (!name) {
showAlert('请输入历史记录名称', 'warning');
return;
}
if (!currentResults) {
showAlert('没有可保存的查询结果', 'warning');
return;
}
try {
const historyData = {
name: name,
description: description,
cluster1_config: getClusterConfig('cluster1'),
cluster2_config: getClusterConfig('cluster2'),
query_options: getQueryOptions(),
query_keys: currentResults.query_keys || [],
results_summary: currentResults.stats || {},
execution_time: currentResults.performance_report?.total_time || 0,
total_keys: currentResults.stats?.total_keys || 0,
different_count: currentResults.stats?.different_count || 0,
identical_count: currentResults.stats?.identical_count || 0,
raw_results: currentResults
};
const response = await fetch('/api/redis/query-history', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(historyData)
});
const result = await response.json();
if (result.success) {
showAlert('Redis查询历史保存成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('saveRedisHistoryModal')).hide();
loadRedisQueryHistory(); // 刷新历史记录列表
} else {
showAlert(`保存失败: ${result.error}`, 'danger');
}
} catch (error) {
console.error('保存Redis查询历史失败:', error);
showAlert(`保存失败: ${error.message}`, 'danger');
}
}
// 刷新Redis查询历史
function refreshRedisQueryHistory() {
loadRedisHistoryForManagement();
}
// 清空所有Redis历史记录
async function clearAllRedisHistory() {
if (!confirm('确定要清空所有Redis查询历史记录吗此操作不可恢复。')) {
return;
}
try {
// 获取所有Redis历史记录
const response = await fetch('/api/redis/query-history');
const result = await response.json();
if (result.success && result.data && result.data.length > 0) {
const allIds = result.data.map(history => history.id);
const deleteResponse = await fetch('/api/redis/query-history/batch-delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
history_ids: allIds
})
});
const deleteResult = await deleteResponse.json();
if (deleteResult.success) {
showAlert(`成功删除 ${deleteResult.deleted_count} 条历史记录`, 'success');
loadRedisHistoryForManagement();
loadRedisQueryHistory(); // 重新加载下拉框
} else {
showAlert(deleteResult.error || '批量删除失败', 'danger');
}
} else {
showAlert('没有历史记录需要删除', 'info');
}
} catch (error) {
console.error('清空Redis历史记录失败:', error);
showAlert(`清空失败: ${error.message}`, 'danger');
}
}
// 切换全选Redis历史记录
function toggleAllRedisHistorySelection() {
const selectAllCheckbox = document.getElementById('selectAllRedisHistory');
const historyCheckboxes = document.querySelectorAll('.redis-history-checkbox');
historyCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
updateRedisHistorySelectionCount();
}
// 更新Redis历史记录选择数量
function updateRedisHistorySelectionCount() {
const selectedCheckboxes = document.querySelectorAll('.redis-history-checkbox:checked');
const count = selectedCheckboxes.length;
const totalCheckboxes = document.querySelectorAll('.redis-history-checkbox');
// 更新显示计数
const countSpan = document.getElementById('selectedRedisHistoryCount');
if (countSpan) {
countSpan.textContent = count;
}
// 更新批量删除按钮状态
const batchDeleteBtn = document.getElementById('batchDeleteRedisHistoryBtn');
if (batchDeleteBtn) {
batchDeleteBtn.disabled = count === 0;
}
// 更新全选复选框状态
const selectAllCheckbox = document.getElementById('selectAllRedisHistory');
if (selectAllCheckbox) {
if (count === 0) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = false;
} else if (count === totalCheckboxes.length) {
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = true;
} else {
selectAllCheckbox.indeterminate = true;
}
}
}
// 批量删除Redis历史记录
async function batchDeleteRedisHistory() {
const selectedCheckboxes = document.querySelectorAll('.redis-history-checkbox:checked');
const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
if (selectedIds.length === 0) {
showAlert('请选择要删除的历史记录', 'warning');
return;
}
if (!confirm(`确定要删除选中的 ${selectedIds.length} 条历史记录吗?此操作不可恢复。`)) {
return;
}
try {
const response = await fetch('/api/redis/query-history/batch-delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
history_ids: selectedIds
})
});
const result = await response.json();
if (result.success) {
showAlert(`成功删除 ${result.deleted_count} 条历史记录`, 'success');
loadRedisHistoryForManagement(); // 重新加载列表
loadRedisQueryHistory(); // 重新加载下拉框
} else {
showAlert(result.error || '批量删除失败', 'danger');
}
} catch (error) {
console.error('批量删除Redis历史记录失败:', error);
showAlert(`批量删除失败: ${error.message}`, 'danger');
}
}
// 过滤Redis模态框日志级别
function filterRedisModalLogsByLevel() {
const showInfo = document.getElementById('redis-modal-log-level-info').checked;
const showWarning = document.getElementById('redis-modal-log-level-warning').checked;
const showError = document.getElementById('redis-modal-log-level-error').checked;
const logContainer = document.getElementById('redis-modal-query-logs');
const logItems = logContainer.querySelectorAll('.log-item');
logItems.forEach(item => {
const level = item.dataset.level;
let show = false;
if (level === 'INFO' && showInfo) show = true;
if (level === 'WARNING' && showWarning) show = true;
if (level === 'ERROR' && showError) show = true;
item.style.display = show ? 'block' : 'none';
});
// 检查每个日志组是否还有可见的日志项
document.querySelectorAll('.redis-log-group').forEach(group => {
const visibleLogs = group.querySelectorAll('.log-item:not([style*="display: none"])');
const cardBody = group.querySelector('.card-body');
if (visibleLogs.length === 0) {
// 如果没有可见的日志项,隐藏整个组
group.style.display = 'none';
} else {
// 如果有可见的日志项,显示组
group.style.display = 'block';
}
});
}
/**
* Redis配置导入功能
*/
// 当前导入的目标集群
let currentImportCluster = null;
// 显示导入配置对话框
function showImportConfigDialog(clusterId) {
currentImportCluster = clusterId;
// 更新模态框标题
const clusterName = clusterId === 'cluster1' ? '集群1 (生产)' : '集群2 (测试)';
document.getElementById('importConfigModalLabel').innerHTML =
`<i class="fas fa-file-import redis-logo"></i> 导入Redis配置 - ${clusterName}`;
// 重置表单
resetImportForm();
// 显示模态框
new bootstrap.Modal(document.getElementById('importConfigModal')).show();
}
// 重置导入表单
function resetImportForm() {
// 重置导入方式选择
document.getElementById('importMethodText').checked = true;
document.getElementById('importMethodFile').checked = false;
// 显示文本导入区域,隐藏文件导入区域
document.getElementById('textImportSection').style.display = 'block';
document.getElementById('fileImportSection').style.display = 'none';
// 清空内容
document.getElementById('configYamlText').value = '';
document.getElementById('configYamlFile').value = '';
// 隐藏预览区域
document.getElementById('configPreview').style.display = 'none';
document.getElementById('previewContent').innerHTML = '';
}
// 绑定导入方式切换事件
document.addEventListener('DOMContentLoaded', function() {
// 导入方式切换
document.querySelectorAll('input[name="importMethod"]').forEach(radio => {
radio.addEventListener('change', function() {
if (this.id === 'importMethodText') {
document.getElementById('textImportSection').style.display = 'block';
document.getElementById('fileImportSection').style.display = 'none';
} else {
document.getElementById('textImportSection').style.display = 'none';
document.getElementById('fileImportSection').style.display = 'block';
}
// 隐藏预览
document.getElementById('configPreview').style.display = 'none';
});
});
// 文件选择事件
document.getElementById('configYamlFile').addEventListener('change', handleFileSelect);
// 拖拽事件
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('click', () => document.getElementById('configYamlFile').click());
dropZone.addEventListener('dragover', handleDragOver);
dropZone.addEventListener('dragleave', handleDragLeave);
dropZone.addEventListener('drop', handleFileDrop);
});
// 处理文件选择
function handleFileSelect(event) {
const file = event.target.files[0];
if (file) {
readFileContent(file);
}
}
// 处理拖拽悬停
function handleDragOver(event) {
event.preventDefault();
event.stopPropagation();
event.currentTarget.classList.add('dragover');
}
// 处理拖拽离开
function handleDragLeave(event) {
event.preventDefault();
event.stopPropagation();
event.currentTarget.classList.remove('dragover');
}
// 处理文件拖拽放置
function handleFileDrop(event) {
event.preventDefault();
event.stopPropagation();
event.currentTarget.classList.remove('dragover');
const files = event.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
// 更新文件输入框
const fileInput = document.getElementById('configYamlFile');
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
fileInput.files = dataTransfer.files;
readFileContent(file);
}
}
// 读取文件内容
function readFileContent(file) {
// 检查文件类型
const validExtensions = ['.yml', '.yaml', '.txt'];
const fileName = file.name.toLowerCase();
const isValidFile = validExtensions.some(ext => fileName.endsWith(ext));
if (!isValidFile) {
showAlert('请选择YAML格式的配置文件.yml, .yaml, .txt', 'warning');
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const content = e.target.result;
document.getElementById('configYamlText').value = content;
showAlert('文件读取成功,请点击"预览配置"查看解析结果', 'success');
};
reader.onerror = function() {
showAlert('文件读取失败,请重试', 'danger');
};
reader.readAsText(file);
}
// 预览配置
function previewConfig() {
const yamlContent = document.getElementById('configYamlText').value.trim();
if (!yamlContent) {
showAlert('请输入YAML配置内容', 'warning');
return;
}
try {
const parsedConfig = parseYamlConfig(yamlContent);
displayConfigPreview(parsedConfig);
document.getElementById('configPreview').style.display = 'block';
} catch (error) {
showAlert(`配置解析失败: ${error.message}`, 'danger');
document.getElementById('configPreview').style.display = 'none';
}
}
// 解析YAML配置
function parseYamlConfig(yamlContent) {
const config = {};
const lines = yamlContent.split('\n');
for (let line of lines) {
line = line.trim();
// 跳过空行和注释
if (!line || line.startsWith('#')) {
continue;
}
// 解析键值对
const colonIndex = line.indexOf(':');
if (colonIndex === -1) {
continue;
}
const key = line.substring(0, colonIndex).trim();
let value = line.substring(colonIndex + 1).trim();
// 移除引号
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
config[key] = value;
}
// 验证必需字段
if (!config.clusterName && !config.name) {
throw new Error('缺少集群名称字段 (clusterName 或 name)');
}
if (!config.clusterAddress && !config.address && !config.nodes) {
throw new Error('缺少集群地址字段 (clusterAddress, address 或 nodes)');
}
// 标准化字段名
const standardConfig = {
clusterName: config.clusterName || config.name || '',
clusterAddress: config.clusterAddress || config.address || config.nodes || '',
clusterPassword: config.clusterPassword || config.password || '',
socketTimeout: config.socketTimeout || config.timeout || 3,
socketConnectTimeout: config.socketConnectTimeout || config.connectTimeout || 3,
maxConnectionsPerNode: config.maxConnectionsPerNode || config.maxConnections || 16
};
return standardConfig;
}
// 显示配置预览
function displayConfigPreview(config) {
const previewContent = document.getElementById('previewContent');
// 解析节点地址
const nodes = parseClusterAddress(config.clusterAddress);
let html = `
<div class="config-preview-item">
<span class="config-preview-label">集群名称:</span>
<span class="config-preview-value">${config.clusterName}</span>
</div>
<div class="config-preview-item">
<span class="config-preview-label">节点数量:</span>
<span class="config-preview-value">${nodes.length} 个节点</span>
</div>
`;
nodes.forEach((node, index) => {
html += `
<div class="config-preview-item">
<span class="config-preview-label">节点 ${index + 1}:</span>
<span class="config-preview-value">${node.host}:${node.port}</span>
</div>
`;
});
html += `
<div class="config-preview-item">
<span class="config-preview-label">密码:</span>
<span class="config-preview-value">${config.clusterPassword ? '已设置' : '未设置'}</span>
</div>
<div class="config-preview-item">
<span class="config-preview-label">连接超时:</span>
<span class="config-preview-value">${config.socketTimeout}秒</span>
</div>
<div class="config-preview-item">
<span class="config-preview-label">连接建立超时:</span>
<span class="config-preview-value">${config.socketConnectTimeout}秒</span>
</div>
<div class="config-preview-item">
<span class="config-preview-label">最大连接数:</span>
<span class="config-preview-value">${config.maxConnectionsPerNode}</span>
</div>
`;
previewContent.innerHTML = html;
}
// 解析集群地址
function parseClusterAddress(addressString) {
if (!addressString) {
return [];
}
const nodes = [];
const addresses = addressString.split(',');
for (let address of addresses) {
address = address.trim();
if (!address) continue;
const parts = address.split(':');
if (parts.length === 2) {
const host = parts[0].trim();
const port = parseInt(parts[1].trim());
if (host && !isNaN(port) && port > 0 && port <= 65535) {
nodes.push({ host, port });
}
}
}
return nodes;
}
// 导入配置
function importConfig() {
const yamlContent = document.getElementById('configYamlText').value.trim();
if (!yamlContent) {
showAlert('请输入YAML配置内容', 'warning');
return;
}
if (!currentImportCluster) {
showAlert('未指定导入目标集群', 'danger');
return;
}
try {
const parsedConfig = parseYamlConfig(yamlContent);
applyConfigToCluster(currentImportCluster, parsedConfig);
// 关闭模态框
bootstrap.Modal.getInstance(document.getElementById('importConfigModal')).hide();
showAlert(`配置已成功导入到${currentImportCluster === 'cluster1' ? '集群1 (生产)' : '集群2 (测试)'}`, 'success');
} catch (error) {
showAlert(`配置导入失败: ${error.message}`, 'danger');
}
}
// 将解析的配置应用到指定集群
function applyConfigToCluster(clusterId, config) {
// 设置集群名称
const nameInput = document.getElementById(`${clusterId}Name`);
if (nameInput) {
nameInput.value = config.clusterName;
}
// 设置密码
const passwordInput = document.getElementById(`${clusterId}Password`);
if (passwordInput) {
passwordInput.value = config.clusterPassword;
}
// 设置连接参数
const timeoutInput = document.getElementById(`${clusterId}SocketTimeout`);
if (timeoutInput) {
timeoutInput.value = config.socketTimeout;
}
const connectTimeoutInput = document.getElementById(`${clusterId}SocketConnectTimeout`);
if (connectTimeoutInput) {
connectTimeoutInput.value = config.socketConnectTimeout;
}
const maxConnectionsInput = document.getElementById(`${clusterId}MaxConnectionsPerNode`);
if (maxConnectionsInput) {
maxConnectionsInput.value = config.maxConnectionsPerNode;
}
// 解析并设置节点
const nodes = parseClusterAddress(config.clusterAddress);
if (nodes.length > 0) {
// 清空现有节点
const nodesContainer = document.getElementById(`${clusterId}Nodes`);
if (nodesContainer) {
nodesContainer.innerHTML = '';
// 添加解析的节点
nodes.forEach((node, index) => {
addNodeToCluster(clusterId, node.host, node.port, index === 0);
});
}
}
}
// 添加节点到集群
function addNodeToCluster(clusterId, host, port, isFirst = false) {
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 form-control-sm me-2" placeholder="127.0.0.1" value="${host}" style="flex: 2;">
<input type="number" class="form-control form-control-sm me-2" placeholder="6379" value="${port}" style="flex: 1;">
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeRedisNode(this, '${clusterId}')">
<i class="fas fa-minus"></i>
</button>
`;
container.appendChild(nodeInput);
// 更新删除按钮状态
updateNodeDeleteButtons();
}
/**
* Redis配置组管理功能增强
*/
// 加载Redis配置组增强版
async function loadRedisConfigGroup(configId) {
try {
const response = await fetch(`/api/redis/config-groups/${configId}`);
const result = await response.json();
if (result.success) {
const config = result.data;
// 应用配置到两个集群
if (config.cluster1_config) {
applyConfigToUI('cluster1', config.cluster1_config);
}
if (config.cluster2_config) {
applyConfigToUI('cluster2', config.cluster2_config);
}
if (config.query_options) {
applyQueryOptionsToUI(config.query_options);
}
// 关闭管理对话框
bootstrap.Modal.getInstance(document.getElementById('manageRedisConfigModal')).hide();
showAlert(`配置组"${config.name}"加载成功`, 'success');
} else {
showAlert(`加载配置组失败: ${result.error}`, 'danger');
}
} catch (error) {
console.error('加载Redis配置组失败:', error);
showAlert(`加载失败: ${error.message}`, 'danger');
}
}
// 应用配置到UI
function applyConfigToUI(clusterId, clusterConfig) {
// 设置集群名称
const nameInput = document.getElementById(`${clusterId}Name`);
if (nameInput && clusterConfig.name) {
nameInput.value = clusterConfig.name;
}
// 设置密码
const passwordInput = document.getElementById(`${clusterId}Password`);
if (passwordInput && clusterConfig.password !== undefined) {
passwordInput.value = clusterConfig.password;
}
// 设置连接参数
if (clusterConfig.socket_timeout !== undefined) {
const timeoutInput = document.getElementById(`${clusterId}SocketTimeout`);
if (timeoutInput) {
timeoutInput.value = clusterConfig.socket_timeout;
}
}
if (clusterConfig.socket_connect_timeout !== undefined) {
const connectTimeoutInput = document.getElementById(`${clusterId}SocketConnectTimeout`);
if (connectTimeoutInput) {
connectTimeoutInput.value = clusterConfig.socket_connect_timeout;
}
}
if (clusterConfig.max_connections_per_node !== undefined) {
const maxConnectionsInput = document.getElementById(`${clusterId}MaxConnectionsPerNode`);
if (maxConnectionsInput) {
maxConnectionsInput.value = clusterConfig.max_connections_per_node;
}
}
// 设置节点
if (clusterConfig.nodes && Array.isArray(clusterConfig.nodes)) {
const nodesContainer = document.getElementById(`${clusterId}Nodes`);
if (nodesContainer) {
nodesContainer.innerHTML = '';
clusterConfig.nodes.forEach((node, index) => {
addNodeToCluster(clusterId, node.host, node.port, index === 0);
});
}
}
}
// 删除Redis配置组
async function deleteRedisConfigGroup(configId, configName) {
if (!confirm(`确定要删除配置组"${configName}"吗?此操作不可恢复。`)) {
return;
}
try {
const response = await fetch(`/api/redis/config-groups/${configId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showAlert(`配置组"${configName}"删除成功`, 'success');
loadRedisConfigGroupsForManagement(); // 刷新列表
} else {
showAlert(`删除失败: ${result.error}`, 'danger');
}
} catch (error) {
console.error('删除Redis配置组失败:', error);
showAlert(`删除失败: ${error.message}`, 'danger');
}
}
// 导出Redis配置组为YAML格式
async function exportRedisConfigGroup(configId, configName) {
try {
const response = await fetch(`/api/redis/config-groups/${configId}`);
const result = await response.json();
if (result.success) {
const config = result.data;
const yamlContent = convertConfigToYaml(config);
// 创建下载链接
const blob = new Blob([yamlContent], { type: 'text/yaml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `redis_config_${configName.replace(/[^a-zA-Z0-9]/g, '_')}.yml`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showAlert(`配置组"${configName}"导出成功`, 'success');
} else {
showAlert(`导出失败: ${result.error}`, 'danger');
}
} catch (error) {
console.error('导出Redis配置组失败:', error);
showAlert(`导出失败: ${error.message}`, 'danger');
}
}
// 将配置转换为YAML格式
function convertConfigToYaml(config) {
let yaml = `# Redis配置组: ${config.name}\n`;
yaml += `# 描述: ${config.description || '无描述'}\n`;
yaml += `# 创建时间: ${config.created_at}\n\n`;
// 集群1配置
if (config.cluster1_config) {
yaml += `# 集群1 (生产环境) 配置\n`;
yaml += `cluster1:\n`;
yaml += ` clusterName: "${config.cluster1_config.name || ''}"\n`;
if (config.cluster1_config.nodes && config.cluster1_config.nodes.length > 0) {
const addresses = config.cluster1_config.nodes.map(node => `${node.host}:${node.port}`).join(',');
yaml += ` clusterAddress: "${addresses}"\n`;
}
yaml += ` clusterPassword: "${config.cluster1_config.password || ''}"\n`;
yaml += ` socketTimeout: ${config.cluster1_config.socket_timeout || 3}\n`;
yaml += ` socketConnectTimeout: ${config.cluster1_config.socket_connect_timeout || 3}\n`;
yaml += ` maxConnectionsPerNode: ${config.cluster1_config.max_connections_per_node || 16}\n\n`;
}
// 集群2配置
if (config.cluster2_config) {
yaml += `# 集群2 (测试环境) 配置\n`;
yaml += `cluster2:\n`;
yaml += ` clusterName: "${config.cluster2_config.name || ''}"\n`;
if (config.cluster2_config.nodes && config.cluster2_config.nodes.length > 0) {
const addresses = config.cluster2_config.nodes.map(node => `${node.host}:${node.port}`).join(',');
yaml += ` clusterAddress: "${addresses}"\n`;
}
yaml += ` clusterPassword: "${config.cluster2_config.password || ''}"\n`;
yaml += ` socketTimeout: ${config.cluster2_config.socket_timeout || 3}\n`;
yaml += ` socketConnectTimeout: ${config.cluster2_config.socket_connect_timeout || 3}\n`;
yaml += ` maxConnectionsPerNode: ${config.cluster2_config.max_connections_per_node || 16}\n\n`;
}
// 查询选项
if (config.query_options) {
yaml += `# 查询选项\n`;
yaml += `queryOptions:\n`;
yaml += ` mode: "${config.query_options.mode || 'random'}"\n`;
yaml += ` count: ${config.query_options.count || 100}\n`;
yaml += ` pattern: "${config.query_options.pattern || '*'}"\n`;
yaml += ` sourceCluster: "${config.query_options.source_cluster || 'cluster1'}"\n`;
}
return yaml;
}
// 复制Redis配置组
async function copyRedisConfigGroup(configId, configName) {
try {
const response = await fetch(`/api/redis/config-groups/${configId}`);
const result = await response.json();
if (result.success) {
const originalConfig = result.data;
// 创建副本配置
const copyConfig = {
name: `${originalConfig.name}_副本`,
description: `${originalConfig.description || ''} (复制自: ${originalConfig.name})`,
cluster1_config: originalConfig.cluster1_config,
cluster2_config: originalConfig.cluster2_config,
query_options: originalConfig.query_options
};
// 保存副本
const saveResponse = await fetch('/api/redis/config-groups', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(copyConfig)
});
const saveResult = await saveResponse.json();
if (saveResult.success) {
showAlert(`配置组"${configName}"复制成功`, 'success');
loadRedisConfigGroupsForManagement(); // 刷新列表
} else {
showAlert(`复制失败: ${saveResult.error}`, 'danger');
}
} else {
showAlert(`获取配置失败: ${result.error}`, 'danger');
}
} catch (error) {
console.error('复制Redis配置组失败:', error);
showAlert(`复制失败: ${error.message}`, 'danger');
}
}
// 测试Redis配置连接
async function testRedisConfigConnection(configId) {
try {
const response = await fetch(`/api/redis/config-groups/${configId}`);
const result = await response.json();
if (result.success) {
const config = result.data;
// 测试集群1连接
if (config.cluster1_config) {
showAlert('正在测试集群1连接...', 'info');
await testSingleClusterConnection(config.cluster1_config, '集群1');
}
// 测试集群2连接
if (config.cluster2_config) {
showAlert('正在测试集群2连接...', 'info');
await testSingleClusterConnection(config.cluster2_config, '集群2');
}
} else {
showAlert(`获取配置失败: ${result.error}`, 'danger');
}
} catch (error) {
console.error('测试Redis配置连接失败:', error);
showAlert(`连接测试失败: ${error.message}`, 'danger');
}
}
// 测试单个集群连接
async function testSingleClusterConnection(clusterConfig, clusterName) {
try {
const testResponse = await fetch('/api/redis/test-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
cluster_config: clusterConfig
})
});
const testResult = await testResponse.json();
if (testResult.success) {
showAlert(`${clusterName}连接测试成功`, 'success');
} else {
showAlert(`${clusterName}连接测试失败: ${testResult.error}`, 'danger');
}
} catch (error) {
showAlert(`${clusterName}连接测试异常: ${error.message}`, 'danger');
}
}
// 应用查询选项到UI
function applyQueryOptionsToUI(queryOptions) {
// 设置查询模式
if (queryOptions.mode === 'random') {
document.getElementById('randomMode').checked = true;
document.getElementById('specifiedMode').checked = false;
} else {
document.getElementById('randomMode').checked = false;
document.getElementById('specifiedMode').checked = true;
}
// 触发模式切换
toggleQueryMode();
// 设置查询参数
if (queryOptions.count !== undefined) {
const countInput = document.getElementById('sampleCount');
if (countInput) {
countInput.value = queryOptions.count;
}
}
if (queryOptions.pattern !== undefined) {
const patternInput = document.getElementById('keyPattern');
if (patternInput) {
patternInput.value = queryOptions.pattern;
}
}
if (queryOptions.source_cluster !== undefined) {
const sourceSelect = document.getElementById('sourceCluster');
if (sourceSelect) {
sourceSelect.value = queryOptions.source_cluster;
}
}
// 设置指定Key列表
if (queryOptions.keys && Array.isArray(queryOptions.keys)) {
const keysTextarea = document.getElementById('specifiedKeys');
if (keysTextarea) {
keysTextarea.value = queryOptions.keys.join('\n');
}
}
}