📋 历史任务
@@ -1168,12 +1190,15 @@ textarea{resize:vertical;min-height:60px}
| {{printf "%.3f" (divf .DownloadBytes 1073741824)}} GB |
{{.StartedAt}} |
{{.FinishedAt}} |
-
-
- |
+
+
+ |
{{end}}
- {{if (not .HistoryTasks)}}| 暂无历史任务 |
{{end}}
+ {{if eq (len .HistoryTasks) 0}}| 暂无历史任务 |
{{end}}
@@ -1212,13 +1237,23 @@ function calcPing(lastSeen) {
if (!lastSeen) return null;
var t = new Date(lastSeen);
if (isNaN(t)) return null;
- return Math.floor((Date.now() - t.getTime()) / 1000);
+ return Date.now() - t.getTime();
+}
+function renderPing(ms) {
+ if (ms === null) return { text: '?', cls: 'ping-warn' };
+ if (ms < 30000) return { text: ms + ' ms', cls: 'ping-ok' };
+ if (ms < 60000) return { text: ms + ' ms', cls: 'ping-warn' };
+ return { text: ms + ' ms ⚠', cls: 'ping-dead' };
}
-function renderPing(sec) {
- if (sec === null) return { text: '?', cls: 'ping-warn' };
- if (sec < 30) return { text: sec + 's', cls: 'ping-ok' };
- if (sec < 60) return { text: sec + 's', cls: 'ping-warn' };
- return { text: sec + 's ⚠', cls: 'ping-dead' };
+function syncRunningTaskLatency() {
+ document.querySelectorAll('#runningTaskBody .rtt-col[data-client-id]').forEach(function(cell) {
+ var cid = cell.dataset.clientId;
+ var crow = document.querySelector('#clientBody tr[data-client-id="' + cid + '"]');
+ var ms = crow ? calcPing(crow.dataset.lastSeen) : null;
+ var r = renderPing(ms);
+ cell.textContent = r.text;
+ cell.className = 'rtt-col ' + r.cls;
+ });
}
function tickPing() {
document.querySelectorAll('#clientBody tr[data-client-id]').forEach(function(row) {
@@ -1229,8 +1264,9 @@ function tickPing() {
cell.textContent = r.text;
cell.className = 'ping-col ' + r.cls;
});
+ syncRunningTaskLatency();
}
-setInterval(tickPing, 1000);
+setInterval(tickPing, 15000);
tickPing();
var knownTaskStatus = {};
@@ -1238,13 +1274,16 @@ var knownTaskStatus = {};
// 动态行:停止按钮用 data-task-id + class="stop-btn",不再内嵌 form
function buildRunningRow(t) {
var name = clientNameMap[t.client_id] || t.client_name || t.client_id;
- var stopBtn = t.status === 'running'
- ? '
'
- : '
-';
+ var stopBtn = t.status === 'running'
+ ? '
'
+ : '
-';
return '
'
+ '| ' + name + ' | ' + t.mode + ' | ' + t.up_mbps + ' | ' + t.down_mbps + ' | '
+ '' + t.duration_sec + ' 秒 | '
+ '' + t.status + ' | '
+ + '- | '
+ '' + fmtGB(t.upload_bytes) + ' | '
+ '' + fmtGB(t.download_bytes) + ' | '
+ '' + t.started_at + ' | '
@@ -1263,12 +1302,14 @@ function buildHistoryRow(t) {
+ '' + fmtGB(t.download_bytes) + ' | '
+ '' + t.started_at + ' | '
+ '' + t.finished_at + ' | '
- + ' | '
- + '
';
+ + '
| '
+ + '';
}
function pollData() {
- fetch('/api/data', {credentials: 'include'})
+ fetch('/api/data', {credentials: 'include', cache: 'no-store'})
.then(function(r) { if (!r.ok) throw new Error(r.status); return r.json(); })
.then(function(data) {
var tasks = data.tasks || [];
@@ -1296,7 +1337,7 @@ function pollData() {
if (needFullRefresh) {
var rb = document.getElementById('runningTaskBody');
if (rb) rb.innerHTML = runningTasks.length === 0
- ? '
| 暂无正在执行的任务 |
'
+ ? '
| 暂无正在执行的任务 |
'
: runningTasks.map(buildRunningRow).join('');
var hb = document.getElementById('historyTaskBody');
if (hb) hb.innerHTML = historyTasks.length === 0
@@ -1317,7 +1358,7 @@ function pollData() {
if (opCell) {
var existBtn = opCell.querySelector('.stop-btn');
if (t.status === 'running' && !existBtn) {
- opCell.innerHTML = '
';
+ opCell.innerHTML = '
';
} else if (t.status !== 'running' && existBtn) {
opCell.innerHTML = '
-';
}
@@ -1356,78 +1397,18 @@ es.onerror = function() {
if (liveStatus) liveStatus.textContent = '实时消息流异常,仍会每 5 秒自动刷新数据。';
};
-// ── 事件委托:停止任务 ──
-document.addEventListener('click', function(e) {
- var btn = e.target.closest('.stop-btn');
- if (!btn) return;
- var taskId = btn.dataset.taskId;
- if (!taskId) return;
- if (!confirm('确认停止此任务?')) return;
- btn.disabled = true;
- btn.textContent = '停止中...';
- apiFetch('/task/stop', 'task_id=' + encodeURIComponent(taskId))
- .then(function(r) {
- if (!r.ok) throw new Error(r.status);
- pollData();
- })
- .catch(function(err) {
- alert('停止失败: ' + err);
- btn.disabled = false;
- btn.textContent = '停止';
- });
-});
-
-// ── 事件委托:删除任务 ──
-document.addEventListener('click', function(e) {
- var btn = e.target.closest('.delete-btn');
- if (!btn) return;
- var taskId = btn.dataset.taskId;
- if (!taskId) return;
- if (!confirm('确认删除此任务记录?')) return;
- btn.disabled = true;
- apiFetch('/task/delete', 'task_id=' + encodeURIComponent(taskId))
- .then(function(r) {
- if (!r.ok) throw new Error(r.status);
- pollData();
- })
- .catch(function(err) {
- alert('删除失败: ' + err);
- btn.disabled = false;
- });
-});
-
-// ── 事件委托:批准客户端 ──
-document.addEventListener('click', function(e) {
- var btn = e.target.closest('.approve-btn');
- if (!btn) return;
- var clientId = btn.dataset.id;
- if (!clientId) return;
- btn.disabled = true;
- btn.textContent = '批准中...';
- apiFetch('/approve', 'client_id=' + encodeURIComponent(clientId))
- .then(function(r) {
- if (!r.ok) throw new Error(r.status);
- location.reload();
- })
- .catch(function(err) {
- alert('批准失败: ' + err);
- btn.disabled = false;
- btn.textContent = '批准';
- });
-});
-
// ── 编辑客户端弹窗 ──
var editClientId = '';
document.getElementById('closeEditBtn').addEventListener('click', function() {
document.getElementById('editModal').classList.remove('open');
});
-document.addEventListener('click', function(e) {
- var btn = e.target.closest('.edit-btn');
- if (!btn) return;
- editClientId = btn.dataset.id;
- document.getElementById('editName').value = btn.dataset.name || '';
- document.getElementById('editRemark').value = btn.dataset.remark || '';
- document.getElementById('editModal').classList.add('open');
+document.querySelectorAll('.edit-btn').forEach(function(btn) {
+ btn.addEventListener('click', function() {
+ editClientId = btn.dataset.id;
+ document.getElementById('editName').value = btn.dataset.name || '';
+ document.getElementById('editRemark').value = btn.dataset.remark || '';
+ document.getElementById('editModal').classList.add('open');
+ });
});
document.getElementById('saveEditBtn').addEventListener('click', function() {
var name = document.getElementById('editName').value.trim();
@@ -1460,41 +1441,23 @@ document.getElementById('copyUpgradeBtn').addEventListener('click', function() {
document.execCommand('copy');
alert('已复制到剪贴板');
});
-document.addEventListener('click', function(e) {
- var btn = e.target.closest('.upgrade-btn');
- if (!btn) return;
- var clientName = btn.dataset.name || '';
- var panelUrl = location.protocol + '//' + PANEL_ADDR;
- var ver = VERSION || 'latest';
- var cmd = "curl --proto '=https' --tlsv1.2 -fsSL "
- + "https://raw.githubusercontent.com/ctsunny/bwtest/main/scripts/install_client.sh"
- + " | bash -s -- "
- + " --server-url " + panelUrl
- + " --init-token " + INIT_TOKEN
- + " --client-name '" + clientName.replace(/'/g, "'\\''") + "'"
- + " --version " + ver;
- document.getElementById('upgradeCmd').value = cmd;
- document.getElementById('upgradeModal').classList.add('open');
+document.querySelectorAll('.upgrade-btn').forEach(function(btn) {
+ btn.addEventListener('click', function() {
+ var clientName = btn.dataset.name || '';
+ var panelUrl = location.protocol + '//' + PANEL_ADDR;
+ var ver = VERSION || 'latest';
+ var cmd = "curl --proto '=https' --tlsv1.2 -fsSL "
+ + "https://raw.githubusercontent.com/ctsunny/bwtest/main/scripts/install_client.sh"
+ + " | bash -s -- "
+ + " --server-url " + panelUrl
+ + " --init-token " + INIT_TOKEN
+ + " --client-name '" + clientName.replace(/'/g, "'\\''") + "'"
+ + " --version " + ver;
+ document.getElementById('upgradeCmd').value = cmd;
+ document.getElementById('upgradeModal').classList.add('open');
+ });
});
-// ── 生成客户端安装命令 ──
- document.getElementById('genBtn').addEventListener('click', function() {
- var name = document.getElementById('genName').value.trim();
- var remark = document.getElementById('genRemark').value.trim();
- var version = document.getElementById('genVersion').value.trim() || 'latest';
- if (!name) { alert('请填写客户端名称'); return; }
- var panelUrl = location.protocol + '//' + PANEL_ADDR;
- var cmd = "curl --proto '=https' --tlsv1.2 -fsSL "
- + "https://raw.githubusercontent.com/ctsunny/bwtest/main/scripts/install_client.sh | bash -s --"
- + " --server-url " + panelUrl
- + " --init-token " + INIT_TOKEN
- + " --client-name '" + name.replace(/'/g, "'\\''") + "'"
- + " --version " + version;
- if (remark) cmd += " --remark '" + remark.replace(/'/g, "'\\''") + "'";
- document.getElementById('cmdText').value = cmd;
- document.getElementById('cmdBox').style.display = 'flex';
- document.getElementById('cmdTip').textContent = '将此命令复制到客户端 VPS 上执行即可完成安装与注册。';
-});
document.getElementById('copyCmdBtn').addEventListener('click', function() {
var el = document.getElementById('cmdText');
el.select();
@@ -1502,17 +1465,44 @@ document.getElementById('copyCmdBtn').addEventListener('click', function() {
alert('已复制到剪贴板');
});
- // ── 手动刷新按钮事件监听 ──
+function closeModalOnBackdrop(modalId) {
+ var modal = document.getElementById(modalId);
+ if (!modal) return;
+ modal.addEventListener('click', function(e) {
+ if (e.target === modal) {
+ modal.classList.remove('open');
+ }
+ });
+}
+closeModalOnBackdrop('editModal');
+closeModalOnBackdrop('upgradeModal');
+document.addEventListener('keydown', function(e) {
+ if (e.key !== 'Escape') return;
+ var opened = document.querySelectorAll('.modal-overlay.open');
+ for (var i = 0; i < opened.length; i++) {
+ opened[i].classList.remove('open');
+ }
+});
+
+ // ── 手动刷新按钮事件监听 ──
document.getElementById('reloadBtn').addEventListener('click', function() {
- location.reload();
- });
+ pollData();
+ if (liveStatus) liveStatus.textContent = '手动刷新完成: ' + new Date().toLocaleTimeString();
+ });
+
+ var historyOpen = false;
+ document.getElementById('toggleHistoryBtn').addEventListener('click', function() {
+ historyOpen = !historyOpen;
+ var card = document.getElementById('historyCard');
+ card.style.display = historyOpen ? 'block' : 'none';
+ this.textContent = historyOpen ? '隐藏历史任务' : '显示历史任务';
+ });
})();