3167 lines
107 KiB
JavaScript
3167 lines
107 KiB
JavaScript
/**
|
||
* 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('redisConfigGroupsList');
|
||
if (!container) {
|
||
console.error('Redis配置组列表容器未找到: redisConfigGroupsList');
|
||
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('redisConfigGroupsList');
|
||
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 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');
|
||
}
|
||
}
|
||
} |