1025 lines
43 KiB
HTML
1025 lines
43 KiB
HTML
|
|
<!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>
|
|||
|
|
|