Files
cunkebao_v3/Moncter/public/database-sync-dashboard.html

1025 lines
43 KiB
HTML
Raw Normal View History

2026-01-05 10:16:20 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据库同步进度看板</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 12px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #333;
font-size: 24px;
font-weight: 600;
}
.status-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
}
.status-idle {
background: #e0e0e0;
color: #666;
}
.status-full_sync {
background: #4caf50;
color: white;
}
.status-incremental_sync {
background: #2196f3;
color: white;
}
.status-error {
background: #f44336;
color: white;
}
.refresh-info {
color: #666;
font-size: 14px;
}
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;
margin-bottom: 12px;
}
.card {
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.card-uniform {
min-height: 200px;
display: flex;
flex-direction: column;
}
.card-uniform .card-title {
flex-shrink: 0;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.card-title {
font-size: 14px;
color: #666;
margin-bottom: 12px;
font-weight: 500;
}
.card-value {
font-size: 28px;
font-weight: 600;
color: #333;
margin-bottom: 6px;
}
.card-label {
font-size: 14px;
color: #999;
}
.progress-card {
grid-column: 1 / -1;
}
.progress-bar-container {
background: #e0e0e0;
border-radius: 8px;
height: 24px;
overflow: hidden;
margin-top: 12px;
position: relative;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #4caf50 0%, #8bc34a 100%);
border-radius: 10px;
transition: width 0.5s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 14px;
}
.progress-bar.error {
background: linear-gradient(90deg, #f44336 0%, #e91e63 100%);
}
.progress-bar.success {
background: linear-gradient(90deg, #4caf50 0%, #8bc34a 100%);
}
.progress-bar.warning {
background: linear-gradient(90deg, #ff9800 0%, #ffc107 100%);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 8px;
margin-top: 12px;
}
.stat-item {
text-align: center;
padding: 10px;
background: #f5f5f5;
border-radius: 6px;
}
.stat-value {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 2px;
}
.stat-label {
font-size: 12px;
color: #999;
}
.time-info {
display: flex;
justify-content: space-between;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e0e0e0;
}
.time-item {
text-align: center;
}
.time-value {
font-size: 16px;
font-weight: 600;
color: #333;
}
.time-label {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.error-card {
background: #fff3cd;
border-left: 4px solid #ffc107;
}
.error-card .card-title {
color: #856404;
}
.error-message {
color: #856404;
margin-top: 8px;
font-size: 13px;
line-height: 1.5;
}
.actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #2196f3;
color: white;
}
.btn-primary:hover {
background: #1976d2;
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-danger:hover {
background: #d32f2f;
}
.btn-warning {
background: #ff9800;
color: white;
}
.btn-warning:hover {
background: #f57c00;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #2196f3;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.footer {
text-align: center;
color: white;
margin-top: 12px;
font-size: 12px;
opacity: 0.8;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 数据库同步进度看板</h1>
<div>
<span class="status-badge" id="statusBadge">加载中...</span>
<span class="refresh-info" id="refreshInfo">最后更新: --</span>
</div>
</div>
<div id="dashboard" class="dashboard">
<div class="loading">
<div class="spinner"></div>
<div>正在加载数据...</div>
</div>
</div>
<div class="footer">
<p>数据每 3 秒自动刷新 | 数据库同步系统</p>
</div>
</div>
<script>
let refreshInterval = null;
// 状态映射
const statusMap = {
'idle': { text: '空闲', class: 'status-idle' },
'full_sync': { text: '全量同步中', class: 'status-full_sync' },
'incremental_sync': { text: '增量同步中', class: 'status-incremental_sync' },
'error': { text: '错误', class: 'status-error' }
};
// 格式化数字
function formatNumber(num) {
if (num === null || num === undefined) return '0';
if (num >= 1000000) {
return (num / 1000000).toFixed(2) + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(2) + 'K';
}
return num.toString();
}
// 格式化时间
function formatTime(seconds) {
if (!seconds || seconds === null) return '--';
if (seconds < 60) {
return Math.round(seconds) + '秒';
}
if (seconds < 3600) {
const mins = Math.floor(seconds / 60);
const secs = Math.round(seconds % 60);
return mins + '分' + secs + '秒';
}
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
return hours + '小时' + mins + '分钟';
}
// 格式化字节为可读单位
function formatBytes(bytes) {
if (!bytes || bytes <= 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0;
let value = bytes;
while (value >= 1024 && i < units.length - 1) {
value /= 1024;
i++;
}
return value.toFixed(2) + ' ' + units[i];
}
// 加载进度数据
async function loadProgress() {
try {
const response = await fetch('/api/database-sync/progress');
// 检查HTTP状态码
if (!response.ok) {
throw new Error('HTTP错误: ' + response.status);
}
const result = await response.json();
// API返回格式: { code: 0, msg: '...', data: {...} }
// code === 0 表示成功
if (result.code === 0 || result.code === 200) {
updateDashboard(result.data || {});
updateRefreshTime();
} else {
console.error('API返回错误:', result);
showError('获取进度失败: ' + (result.msg || result.message || '未知错误'));
}
} catch (error) {
console.error('加载进度失败:', error);
const errorMsg = error.message || '网络连接失败';
showError('网络错误: ' + errorMsg + '。请检查服务器是否运行,或稍后重试。');
}
}
// 更新看板
function updateDashboard(data) {
const dashboard = document.getElementById('dashboard');
const status = data.status || 'idle';
const statusInfo = statusMap[status] || { text: status, class: 'status-idle' };
// 更新状态徽章
const statusBadge = document.getElementById('statusBadge');
statusBadge.textContent = statusInfo.text;
statusBadge.className = 'status-badge ' + statusInfo.class;
// 构建 HTML
let html = '';
// 进度卡片
const progressPercent = Math.max(0, Math.min(100, data.progress_percent || 0));
const bytesInfo = data.bytes || {};
const bytesProcessed = bytesInfo.processed || 0;
const bytesTotal = bytesInfo.total || 0;
// 计算进度条颜色(根据进度百分比)
let progressBarClass = '';
if (status === 'error') {
progressBarClass = 'error';
} else if (progressPercent >= 90) {
progressBarClass = 'success';
} else if (progressPercent >= 50) {
progressBarClass = 'warning';
}
html += `
<div class="card progress-card">
<div class="card-title">同步进度</div>
<div class="card-value">${progressPercent.toFixed(2)}%</div>
<div class="progress-bar-container">
<div class="progress-bar ${progressBarClass}" style="width: ${progressPercent}%">
${progressPercent > 5 ? progressPercent.toFixed(1) + '%' : ''}
</div>
</div>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">${data.databases?.completed || 0}/${data.databases?.total || 0}</div>
<div class="stat-label">数据库</div>
</div>
<div class="stat-item">
<div class="stat-value">${data.collections?.completed || 0}/${data.collections?.total || 0}</div>
<div class="stat-label">集合</div>
</div>
<div class="stat-item">
<div class="stat-value">${formatNumber(data.documents?.processed || 0)}/${formatNumber(data.documents?.total || 0)}</div>
<div class="stat-label">文档</div>
</div>
<div class="stat-item">
<div class="stat-value">${bytesTotal > 0 ? formatBytes(bytesProcessed) + ' / ' + formatBytes(bytesTotal) : '--'}</div>
<div class="stat-label">数据量</div>
</div>
</div>
<div class="time-info">
<div class="time-item">
<div class="time-value">${data.current_database || '无'}</div>
<div class="time-label">当前数据库</div>
</div>
<div class="time-item">
<div class="time-value">${data.current_collection || '无'}</div>
<div class="time-label">当前集合</div>
</div>
<div class="time-item">
<div class="time-value">${formatTime(data.time?.elapsed_seconds)}</div>
<div class="time-label">已用时间</div>
</div>
<div class="time-item">
<div class="time-value">${formatTime(data.time?.estimated_remaining_seconds)}</div>
<div class="time-label">预计剩余</div>
</div>
</div>
</div>
`;
// 统计卡片
const stats = data.stats || {};
html += `
<div class="card card-uniform">
<div class="card-title">📈 统计信息</div>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value" style="color: #4caf50">${formatNumber(stats.documents_inserted || 0)}</div>
<div class="stat-label">已插入</div>
</div>
<div class="stat-item">
<div class="stat-value" style="color: #2196f3">${formatNumber(stats.documents_updated || 0)}</div>
<div class="stat-label">已更新</div>
</div>
<div class="stat-item">
<div class="stat-value" style="color: #ff9800">${formatNumber(stats.documents_deleted || 0)}</div>
<div class="stat-label">已删除</div>
</div>
<div class="stat-item">
<div class="stat-value" style="color: ${(stats.errors || 0) > 0 ? '#f44336' : '#4caf50'}">${stats.errors || 0}</div>
<div class="stat-label">错误数</div>
</div>
</div>
</div>
`;
// 时间信息卡片
html += `
<div class="card card-uniform">
<div class="card-title">⏰ 时间信息</div>
<div style="margin-top: 12px;">
<div style="margin-bottom: 10px;">
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">开始时间</div>
<div style="font-size: 15px; font-weight: 600; color: #333;">
${data.time?.start_time || '未开始'}
</div>
</div>
<div style="margin-bottom: 10px;">
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">已用时间</div>
<div style="font-size: 15px; font-weight: 600; color: #333;">
${formatTime(data.time?.elapsed_seconds)}
</div>
</div>
<div>
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">预计剩余时间</div>
<div style="font-size: 15px; font-weight: 600; color: #333;">
${formatTime(data.time?.estimated_remaining_seconds) || '计算中...'}
</div>
</div>
</div>
</div>
`;
// 性能指标卡片(同步速度、吞吐量等)
const elapsedSeconds = data.time?.elapsed_seconds || 0;
const documentsProcessed = data.documents?.processed || 0;
// bytesProcessed 和 bytesTotal 已在上面声明第411-412行直接使用
// 计算同步速度
let docsPerSecond = 0;
let mbPerSecond = 0;
if (elapsedSeconds > 0) {
docsPerSecond = documentsProcessed / elapsedSeconds;
mbPerSecond = bytesProcessed / elapsedSeconds / 1024 / 1024;
}
// 计算平均速度(最近一段时间)
const recentSpeed = docsPerSecond > 0 ? docsPerSecond : 0;
html += `
<div class="card card-uniform">
<div class="card-title">⚡ 性能指标</div>
<div style="margin-top: 12px;">
<div style="margin-bottom: 10px;">
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">文档同步速度</div>
<div style="font-size: 15px; font-weight: 600; color: #333;">
${formatNumber(Math.round(recentSpeed))} 文档/秒
</div>
</div>
<div style="margin-bottom: 10px;">
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">数据吞吐量</div>
<div style="font-size: 15px; font-weight: 600; color: #333;">
${mbPerSecond > 0 ? mbPerSecond.toFixed(2) : '0.00'} MB/秒
</div>
</div>
<div>
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">总处理量</div>
<div style="font-size: 15px; font-weight: 600; color: #333;">
${formatNumber(documentsProcessed)} 文档 / ${formatBytes(bytesProcessed)}
</div>
</div>
</div>
</div>
`;
// 数据库连接状态卡片(移动到性能指标之后)
if (data.connection_status) {
const connStatus = data.connection_status;
let connHtml = '';
// 源数据库连接状态
const sourceConnected = connStatus.source?.connected || false;
const sourceHost = connStatus.source?.host || '未知';
const sourcePort = connStatus.source?.port || 0;
const sourceError = connStatus.source?.error || null;
// 目标数据库连接状态
const targetConnected = connStatus.target?.connected || false;
const targetHost = connStatus.target?.host || '未知';
const targetPort = connStatus.target?.port || 0;
const targetError = connStatus.target?.error || null;
connHtml += `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-top: 12px;">
<div style="padding: 10px; background: ${sourceConnected ? '#e8f5e9' : '#ffebee'}; border-radius: 6px; border-left: 3px solid ${sourceConnected ? '#4caf50' : '#f44336'};">
<div style="font-weight: 600; color: #333; margin-bottom: 6px; font-size: 13px;">
<span style="color: ${sourceConnected ? '#4caf50' : '#f44336'}; margin-right: 6px;">${sourceConnected ? '●' : '○'}</span>
源数据库
</div>
<div style="font-size: 11px; color: #666; margin-top: 6px;">
<div><strong>地址:</strong> ${sourceHost}:${sourcePort}</div>
<div style="margin-top: 4px;"><strong>状态:</strong>
<span style="color: ${sourceConnected ? '#4caf50' : '#f44336'}; font-weight: 600;">
${sourceConnected ? '已连接' : '未连接'}
</span>
</div>
${sourceError ? `<div style="margin-top: 4px; font-size: 10px; color: #f44336;">错误: ${sourceError.substring(0, 50)}${sourceError.length > 50 ? '...' : ''}</div>` : ''}
</div>
</div>
<div style="padding: 10px; background: ${targetConnected ? '#e8f5e9' : '#ffebee'}; border-radius: 6px; border-left: 3px solid ${targetConnected ? '#4caf50' : '#f44336'};">
<div style="font-weight: 600; color: #333; margin-bottom: 6px; font-size: 13px;">
<span style="color: ${targetConnected ? '#4caf50' : '#f44336'}; margin-right: 6px;">${targetConnected ? '●' : '○'}</span>
目标数据库
</div>
<div style="font-size: 11px; color: #666; margin-top: 6px;">
<div><strong>地址:</strong> ${targetHost}:${targetPort}</div>
<div style="margin-top: 4px;"><strong>状态:</strong>
<span style="color: ${targetConnected ? '#4caf50' : '#f44336'}; font-weight: 600;">
${targetConnected ? '已连接' : '未连接'}
</span>
</div>
${targetError ? `<div style="margin-top: 4px; font-size: 10px; color: #f44336;">错误: ${targetError.substring(0, 50)}${targetError.length > 50 ? '...' : ''}</div>` : ''}
</div>
</div>
</div>
`;
// 总体连接状态
const allConnected = sourceConnected && targetConnected;
const overallBgColor = allConnected ? '#e8f5e9' : '#fff3cd';
const overallBorderColor = allConnected ? '#4caf50' : '#ffc107';
const overallTextColor = allConnected ? '#2e7d32' : '#856404';
const overallStatusText = allConnected ? '✓ 所有数据库连接正常' : '⚠ 部分数据库连接异常';
connHtml += `
<div style="margin-top: 12px; padding: 10px; background: ${overallBgColor}; border-radius: 6px; border-left: 3px solid ${overallBorderColor};">
<div style="font-size: 12px; color: ${overallTextColor};">
<strong>总体状态:</strong> ${overallStatusText}
</div>
</div>
`;
html += `
<div class="card card-uniform">
<div class="card-title">🔌 数据库连接状态</div>
${connHtml}
</div>
`;
}
// 数据库列表卡片(已完成、处理中、待同步)
if (data.database_list) {
const dbList = data.database_list;
let dbListHtml = '';
// 处理中的数据库
if (dbList.processing && dbList.processing.length > 0) {
dbListHtml += `
<div style="margin-bottom: 12px;">
<div style="font-size: 12px; color: #666; margin-bottom: 6px; font-weight: 600;">
处理中 (${dbList.processing.length})
</div>
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
`;
dbList.processing.forEach(db => {
dbListHtml += `
<span style="padding: 4px 8px; background: #e3f2fd; color: #1976d2; border-radius: 4px; font-size: 11px;">
${db.name}
</span>
`;
});
dbListHtml += `</div></div>`;
}
// 已完成的数据库
if (dbList.completed && dbList.completed.length > 0) {
dbListHtml += `
<div style="margin-bottom: 12px;">
<div style="font-size: 12px; color: #666; margin-bottom: 6px; font-weight: 600;">
已完成 (${dbList.completed.length})
</div>
<div style="display: flex; flex-wrap: wrap; gap: 6px; max-height: 120px; overflow-y: auto;">
`;
dbList.completed.slice(0, 20).forEach(db => {
dbListHtml += `
<span style="padding: 4px 8px; background: #e8f5e9; color: #2e7d32; border-radius: 4px; font-size: 11px;">
${db.name}
</span>
`;
});
if (dbList.completed.length > 20) {
dbListHtml += `<span style="padding: 4px 8px; color: #999; font-size: 11px;">...还有 ${dbList.completed.length - 20} 个</span>`;
}
dbListHtml += `</div></div>`;
}
// 待同步的数据库
if (dbList.pending && dbList.pending.length > 0) {
dbListHtml += `
<div>
<div style="font-size: 12px; color: #666; margin-bottom: 6px; font-weight: 600;">
待同步 (${dbList.pending.length})
</div>
<div style="display: flex; flex-wrap: wrap; gap: 6px; max-height: 120px; overflow-y: auto;">
`;
dbList.pending.slice(0, 20).forEach(db => {
dbListHtml += `
<span style="padding: 4px 8px; background: #fff3e0; color: #e65100; border-radius: 4px; font-size: 11px;">
${db.name}
</span>
`;
});
if (dbList.pending.length > 20) {
dbListHtml += `<span style="padding: 4px 8px; color: #999; font-size: 11px;">...还有 ${dbList.pending.length - 20} 个</span>`;
}
dbListHtml += `</div></div>`;
}
if (dbListHtml) {
html += `
<div class="card" style="grid-column: 1 / -1;">
<div class="card-title">📋 数据库列表</div>
<div style="margin-top: 12px;">
${dbListHtml}
</div>
</div>
`;
}
}
// 错误信息卡片
if (status === 'error') {
const errorDb = data.error_database || data.current_database || '未知';
const errorCollection = data.current_collection || (data.last_error?.collection) || '未知';
const errorMessage = data.last_error?.message || '未知错误';
const errorTime = data.last_error?.time || '未知';
const errorFile = data.last_error?.file || '';
const errorLine = data.last_error?.line || '';
html += `
<div class="card error-card">
<div class="card-title">⚠️ 错误信息</div>
<div class="error-message">
<div><strong>数据库:</strong> ${errorDb}</div>
<div><strong>集合:</strong> ${errorCollection}</div>
<div><strong>错误:</strong> ${errorMessage}</div>
<div><strong>时间:</strong> ${errorTime}</div>
${errorFile ? `<div style="margin-top: 8px; font-size: 12px; color: #666;"><strong>位置:</strong> ${errorFile}${errorLine ? ':' + errorLine : ''}</div>` : ''}
</div>
<div class="actions">
<button class="btn btn-warning" onclick="skipError()">跳过错误数据库</button>
</div>
</div>
`;
}
// Worker 状态卡片(多进程状态)
if (data.worker_status) {
const workerStatus = data.worker_status;
let workerHtml = '';
if (workerStatus.workers && workerStatus.workers.length > 0) {
// 显示每个 Worker 的详细信息
workerHtml += `
<div style="margin-top: 12px;">
<div style="margin-bottom: 10px; font-size: 13px; color: #666;">
总 Worker 数: <strong>${workerStatus.total_workers || 0}</strong> |
活跃 Worker 数: <strong style="color: #4caf50;">${workerStatus.active_workers || 0}</strong>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px; margin-top: 10px;">
`;
// 按 Worker ID 排序
const sortedWorkers = Object.values(workerStatus.workers).sort((a, b) => a.worker_id - b.worker_id);
sortedWorkers.forEach(worker => {
const dbCount = worker.databases ? worker.databases.length : 0;
workerHtml += `
<div style="padding: 10px; background: #f5f5f5; border-radius: 6px; border-left: 3px solid #4caf50;">
<div style="font-weight: 600; color: #333; margin-bottom: 6px; font-size: 13px;">
Worker #${worker.worker_id}
<span style="font-size: 11px; color: #4caf50; margin-left: 6px;">● 运行中</span>
</div>
<div style="font-size: 11px; color: #666; margin-top: 6px;">
<div>数据库: <strong>${dbCount}</strong></div>
<div>集合: <strong>${worker.collections || 0}</strong></div>
<div>文档: <strong>${formatNumber(worker.documents_processed || 0)}</strong></div>
${dbCount > 0 ? `<div style="margin-top: 6px; font-size: 10px; color: #999;">${worker.databases.slice(0, 3).join(', ')}${dbCount > 3 ? '...' : ''}</div>` : ''}
</div>
</div>
`;
});
workerHtml += `</div>`;
} else if (workerStatus.workers && workerStatus.workers.length > 0) {
// 显示所有 Worker包括等待状态的
workerHtml += `
<div style="margin-top: 12px;">
<div style="margin-bottom: 10px; font-size: 13px; color: #666;">
总 Worker 数: <strong>${workerStatus.total_workers || 0}</strong> |
活跃 Worker 数: <strong style="color: #4caf50;">${workerStatus.active_workers || 0}</strong>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px; margin-top: 10px;">
`;
// 按 Worker ID 排序
const sortedWorkers = Object.values(workerStatus.workers).sort((a, b) => a.worker_id - b.worker_id);
sortedWorkers.forEach(worker => {
const dbCount = worker.databases ? worker.databases.length : 0;
const isActive = worker.status === 'active' && dbCount > 0;
const statusColor = isActive ? '#4caf50' : '#999';
const statusText = isActive ? '● 运行中' : '○ 等待中';
const borderColor = isActive ? '#4caf50' : '#e0e0e0';
workerHtml += `
<div style="padding: 10px; background: #f5f5f5; border-radius: 6px; border-left: 3px solid ${borderColor};">
<div style="font-weight: 600; color: #333; margin-bottom: 6px; font-size: 13px;">
Worker #${worker.worker_id}
<span style="font-size: 11px; color: ${statusColor}; margin-left: 6px;">${statusText}</span>
</div>
<div style="font-size: 11px; color: #666; margin-top: 6px;">
<div>数据库: <strong>${dbCount}</strong></div>
<div>集合: <strong>${worker.collections || 0}</strong></div>
<div>文档: <strong>${formatNumber(worker.documents_processed || 0)}</strong></div>
${dbCount > 0 ? `<div style="margin-top: 6px; font-size: 10px; color: #999;">${worker.databases.slice(0, 3).join(', ')}${dbCount > 3 ? '...' : ''}</div>` : '<div style="margin-top: 6px; font-size: 10px; color: #999;">等待分配任务...</div>'}
</div>
</div>
`;
});
workerHtml += `</div>`;
if (workerStatus.message) {
workerHtml += `
<div style="margin-top: 12px; padding: 10px; background: #fff3cd; border-radius: 6px; border-left: 3px solid #ffc107;">
<div style="color: #856404; font-size: 12px;">
${workerStatus.message}
</div>
</div>
`;
}
} else {
// 完全没有 Worker 信息时的提示
workerHtml += `
<div style="margin-top: 12px; padding: 12px; background: #fff3cd; border-radius: 6px; border-left: 3px solid #ffc107;">
<div style="color: #856404; font-size: 13px;">
${workerStatus.message || '暂无 Worker 活动信息'}
</div>
${workerStatus.total_workers > 0 ? `<div style="margin-top: 6px; font-size: 11px; color: #666;">配置的 Worker 数量: ${workerStatus.total_workers}</div>` : ''}
</div>
`;
}
if (data.progress_file_last_modified) {
const ageSeconds = data.progress_file_age_seconds || 0;
const ageText = ageSeconds < 60 ? `${ageSeconds}秒前` :
ageSeconds < 3600 ? `${Math.floor(ageSeconds / 60)}分钟前` :
`${Math.floor(ageSeconds / 3600)}小时前`;
workerHtml += `
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #e0e0e0;">
<div style="font-size: 11px; color: #999;">
进度文件最后修改: ${data.progress_file_last_modified} (${ageText})
</div>
</div>
`;
}
if (data.hint) {
workerHtml += `
<div style="margin-top: 10px; padding: 10px; background: #e3f2fd; border-radius: 6px; border-left: 3px solid #2196f3;">
<div style="font-size: 11px; color: #1976d2;">
💡 ${data.hint}
</div>
</div>
`;
}
html += `
<div class="card" style="grid-column: 1 / -1;">
<div class="card-title">🔧 Worker 进程状态</div>
${workerHtml}
</div>
`;
}
// 操作按钮
html += `
<div class="card">
<div class="card-title">⚙️ 操作</div>
<div class="actions">
<button class="btn btn-primary" onclick="loadProgress()">🔄 刷新</button>
<button class="btn btn-danger" onclick="resetProgress()">🗑️ 重置进度</button>
${status === 'error' ? '<button class="btn btn-warning" onclick="skipError()">⏭️ 跳过错误</button>' : ''}
</div>
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #e0e0e0;">
<div style="font-size: 11px; color: #999; line-height: 1.5;">
<div>💡 提示:</div>
<div style="margin-top: 2px;">• 页面每 3 秒自动刷新</div>
<div>• 重置进度将清除所有同步进度信息</div>
<div>• 跳过错误将标记当前错误数据库为已完成</div>
</div>
</div>
</div>
`;
dashboard.innerHTML = html;
}
// 显示错误
function showError(message) {
const dashboard = document.getElementById('dashboard');
dashboard.innerHTML = `
<div class="card" style="grid-column: 1 / -1;">
<div class="card-title" style="color: #f44336;">❌ 错误</div>
<div style="color: #666; margin-top: 16px;">${message}</div>
<div class="actions" style="margin-top: 20px;">
<button class="btn btn-primary" onclick="loadProgress()">重试</button>
</div>
</div>
`;
}
// 更新刷新时间
function updateRefreshTime() {
const now = new Date();
const timeStr = now.toLocaleTimeString('zh-CN');
document.getElementById('refreshInfo').textContent = '最后更新: ' + timeStr;
}
// 跳过错误数据库
async function skipError() {
if (!confirm('确定要跳过当前错误数据库吗?')) {
return;
}
try {
const response = await fetch('/api/database-sync/skip-error', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('HTTP错误: ' + response.status);
}
const result = await response.json();
// code === 0 表示成功
if (result.code === 0 || result.code === 200) {
alert('已跳过错误数据库');
loadProgress();
} else {
alert('操作失败: ' + (result.msg || result.message || '未知错误'));
}
} catch (error) {
alert('网络错误: ' + error.message);
}
}
// 重置进度
async function resetProgress() {
if (!confirm('确定要重置同步进度吗?这将清除所有进度信息。')) {
return;
}
try {
const response = await fetch('/api/database-sync/reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('HTTP错误: ' + response.status);
}
const result = await response.json();
// code === 0 表示成功
if (result.code === 0 || result.code === 200) {
alert('进度已重置');
loadProgress();
} else {
alert('操作失败: ' + (result.msg || result.message || '未知错误'));
}
} catch (error) {
alert('网络错误: ' + error.message);
}
}
// 初始化
document.addEventListener('DOMContentLoaded', function() {
// 立即加载一次
loadProgress();
// 每3秒刷新一次
refreshInterval = setInterval(loadProgress, 3000);
});
// 页面可见性变化时暂停/恢复刷新
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
} else {
if (!refreshInterval) {
loadProgress();
refreshInterval = setInterval(loadProgress, 3000);
}
}
});
</script>
</body>
</html>