添加 templates/index.html
This commit is contained in:
parent
145ab01d4f
commit
c410ec8a58
927
templates/index.html
Normal file
927
templates/index.html
Normal file
@ -0,0 +1,927 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>客户端控制面板</title>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { padding: 20px; }
|
||||||
|
.config { margin-bottom: 20px; }
|
||||||
|
.actions { margin-bottom: 20px; }
|
||||||
|
#commandResults {
|
||||||
|
margin-top: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.result-item {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.result-item pre {
|
||||||
|
margin: 5px 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.modal-dialog {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: calc(100% - 1rem);
|
||||||
|
}
|
||||||
|
#manualCommand {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
.group-tag {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.group-tag.active {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
#clientTable tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#clientTable tbody tr.selected {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
#selectionBox {
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid #007bff;
|
||||||
|
background-color: rgba(0, 123, 255, 0.1);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="selectionBox" style="display: none;"></div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row mt-3 mb-3">
|
||||||
|
<div class="col-md-12 text-end">
|
||||||
|
<a href="/logout" class="btn btn-secondary">登出</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 class="mb-4">客户端控制面板</h1>
|
||||||
|
<div class="config row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="heartbeatTime" class="form-label">心跳时间(秒):</label>
|
||||||
|
<input type="number" id="heartbeatTime" class="form-control" value="30">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="reportTime" class="form-label">报告时间(秒):</label>
|
||||||
|
<input type="number" id="reportTime" class="form-control" value="10">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mt-4">
|
||||||
|
<button onclick="updateSettings()" class="btn btn-primary">更新设置</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="configSelector" class="form-label">选择配置:</label>
|
||||||
|
<select id="configSelector" class="form-select" onchange="loadSelectedConfig()">
|
||||||
|
<option value="">-- 选择配置 --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="configName" class="form-label">配置名称:</label>
|
||||||
|
<input type="text" id="configName" class="form-control" placeholder="输入新配置名称">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mt-4">
|
||||||
|
<button onclick="saveConfig()" class="btn btn-success">保存配置</button>
|
||||||
|
<button onclick="deleteConfig()" class="btn btn-danger">删除配置</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="updateUrl" class="form-label">更新URL:</label>
|
||||||
|
<input type="text" id="updateUrl" class="form-control" value="https://example.com/update">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="programName" class="form-label">程序名:</label>
|
||||||
|
<input type="text" id="programName" class="form-control" value="example-program">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="programPath" class="form-label">程序路径:</label>
|
||||||
|
<input type="text" id="programPath" class="form-control" value="/path/to/program">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="commandParams" class="form-label">命令参数:</label>
|
||||||
|
<input type="text" id="commandParams" class="form-control" value="--param value">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="group-selector row mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label">选择分组:</label>
|
||||||
|
<div id="groupTags"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mt-2">
|
||||||
|
<input type="text" id="newGroupName" class="form-control" placeholder="新分组名称">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mt-2">
|
||||||
|
<button onclick="addNewGroup()" class="btn btn-secondary">添加新分组</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button onclick="executeCommand('start')" class="btn btn-success">启动程序</button>
|
||||||
|
<button onclick="executeCommand('stop')" class="btn btn-danger">停止程序</button>
|
||||||
|
<button onclick="executeCommand('update')" class="btn btn-info">更新程序</button>
|
||||||
|
<button onclick="executeCommand('restart')" class="btn btn-warning">重启程序</button>
|
||||||
|
<button onclick="showManualCommandModal()" class="btn btn-secondary">手动命令</button>
|
||||||
|
<button onclick="deleteSelectedClients()" class="btn btn-danger">删除选中客户端</button>
|
||||||
|
<button onclick="changeBulkClientGroup()" class="btn btn-info">批量更改分组</button>
|
||||||
|
<button onclick="executeCommand('force_update')" class="btn btn-warning">强制更新</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<button onclick="selectAllClients()" class="btn btn-outline-primary">全选</button>
|
||||||
|
<button onclick="deselectAllClients()" class="btn btn-outline-secondary">取消全选</button>
|
||||||
|
</div>
|
||||||
|
<table id="clientTable" class="table table-hover mt-3">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>客户端ID</th>
|
||||||
|
<th>IP地址</th>
|
||||||
|
<th>CPU使用率</th>
|
||||||
|
<th>命令状态</th>
|
||||||
|
<th>程序状态</th>
|
||||||
|
<th>分组</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div id="commandResults">
|
||||||
|
<h3>命令执行结果</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 手动命令模态框 -->
|
||||||
|
<div class="modal fade" id="manualCommandModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">输入手动命令</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<textarea id="manualCommand" class="form-control" placeholder="输入命令"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="executeManualCommand()">执行</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const ACCESS_TOKEN = '131417';
|
||||||
|
|
||||||
|
const socket = io('http://ore.uqdm.com:5003', {
|
||||||
|
transports: ['websocket', 'polling'],
|
||||||
|
upgrade: true,
|
||||||
|
auth: {
|
||||||
|
token: ACCESS_TOKEN
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let clients = {};
|
||||||
|
let groups = [];
|
||||||
|
let manualCommandModal;
|
||||||
|
let selectedClients = new Set();
|
||||||
|
let currentGroup = 'all';
|
||||||
|
let isMouseDown = false;
|
||||||
|
let startX, startY;
|
||||||
|
const selectionBox = document.getElementById('selectionBox');
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
console.log('Connected to server');
|
||||||
|
fetchClients();
|
||||||
|
loadConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('command_result', (data) => {
|
||||||
|
console.log('Received real-time command result:', data);
|
||||||
|
displayCommandResult(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
function fetchClients() {
|
||||||
|
console.log('Fetching clients...');
|
||||||
|
fetch('/api/clients', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log('Received client data:', data);
|
||||||
|
if (data.clients && Object.keys(data.clients).length > 0) {
|
||||||
|
clients = data.clients;
|
||||||
|
groups = data.groups;
|
||||||
|
updateClientTable();
|
||||||
|
updateGroupTags();
|
||||||
|
} else {
|
||||||
|
console.warn('No clients data received');
|
||||||
|
document.querySelector('#clientTable tbody').innerHTML = '<tr><td colspan="8">No clients found</td></tr>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching clients:', error);
|
||||||
|
document.querySelector('#clientTable tbody').innerHTML = '<tr><td colspan="8">Error loading clients</td></tr>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateClientTable() {
|
||||||
|
console.log('Updating client table');
|
||||||
|
console.log('Current group:', currentGroup);
|
||||||
|
console.log('Clients:', clients);
|
||||||
|
|
||||||
|
const tbody = document.querySelector('#clientTable tbody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
let clientCount = 0;
|
||||||
|
|
||||||
|
for (const [clientId, data] of Object.entries(clients)) {
|
||||||
|
if (currentGroup === 'all' || data.group === currentGroup) {
|
||||||
|
clientCount++;
|
||||||
|
const row = tbody.insertRow();
|
||||||
|
row.dataset.clientId = clientId;
|
||||||
|
row.className = selectedClients.has(clientId) ? 'selected' : '';
|
||||||
|
|
||||||
|
// 添加序号列
|
||||||
|
row.insertCell(0).textContent = clientCount;
|
||||||
|
|
||||||
|
row.insertCell(1).textContent = clientId;
|
||||||
|
row.insertCell(2).textContent = data.system_info?.ip_address || 'N/A';
|
||||||
|
row.insertCell(3).textContent = data.system_info?.cpu || 'N/A';
|
||||||
|
|
||||||
|
const commandStatusCell = row.insertCell(4);
|
||||||
|
if (data.command_status && typeof data.command_status === 'object') {
|
||||||
|
commandStatusCell.textContent = data.command_status.status || 'N/A';
|
||||||
|
commandStatusCell.title = data.command_status.output || data.command_status.error || '';
|
||||||
|
} else {
|
||||||
|
commandStatusCell.textContent = 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
row.insertCell(5).textContent = data.system_info?.program_status || 'N/A';
|
||||||
|
row.insertCell(6).textContent = data.group || 'default';
|
||||||
|
row.insertCell(7).innerHTML = `<button onclick="changeClientGroup('${clientId}')" class="btn btn-sm btn-outline-primary">更改分组</button>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Updated table with ${clientCount} clients`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGroupTags() {
|
||||||
|
const groupTags = document.getElementById('groupTags');
|
||||||
|
groupTags.innerHTML = '';
|
||||||
|
const allTag = document.createElement('span');
|
||||||
|
allTag.className = `group-tag ${currentGroup === 'all' ? 'active' : ''}`;
|
||||||
|
allTag.textContent = '全部';
|
||||||
|
allTag.onclick = () => selectGroup('all');
|
||||||
|
groupTags.appendChild(allTag);
|
||||||
|
|
||||||
|
groups.forEach(group => {
|
||||||
|
const tag = document.createElement('span');
|
||||||
|
tag.className = `group-tag ${currentGroup === group ? 'active' : ''}`;
|
||||||
|
tag.textContent = group;
|
||||||
|
tag.onclick = () => selectGroup(group);
|
||||||
|
if (group !== 'default' && group !== '离线') {
|
||||||
|
const deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.textContent = 'x';
|
||||||
|
deleteBtn.className = 'btn btn-sm btn-danger ml-1';
|
||||||
|
deleteBtn.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteGroup(group);
|
||||||
|
};
|
||||||
|
tag.appendChild(deleteBtn);
|
||||||
|
}
|
||||||
|
groupTags.appendChild(tag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectGroup(group) {
|
||||||
|
currentGroup = group;
|
||||||
|
updateGroupTags();
|
||||||
|
updateClientTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleClientSelection(clientId, row, addToSelection = true) {
|
||||||
|
if (addToSelection) {
|
||||||
|
if (selectedClients.has(clientId)) {
|
||||||
|
selectedClients.delete(clientId);
|
||||||
|
row.classList.remove('selected');
|
||||||
|
} else {
|
||||||
|
selectedClients.add(clientId);
|
||||||
|
row.classList.add('selected');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (selectedClients.has(clientId)) {
|
||||||
|
selectedClients.delete(clientId);
|
||||||
|
row.classList.remove('selected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllClients() {
|
||||||
|
const rows = document.querySelectorAll('#clientTable tbody tr');
|
||||||
|
rows.forEach(row => {
|
||||||
|
const clientId = row.dataset.clientId;
|
||||||
|
selectedClients.add(clientId);
|
||||||
|
row.classList.add('selected');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deselectAllClients() {
|
||||||
|
const rows = document.querySelectorAll('#clientTable tbody tr');
|
||||||
|
rows.forEach(row => {
|
||||||
|
const clientId = row.dataset.clientId;
|
||||||
|
selectedClients.delete(clientId);
|
||||||
|
row.classList.remove('selected');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedClients() {
|
||||||
|
return Array.from(selectedClients);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateClientStatus(clientId, data) {
|
||||||
|
console.log('Updating client status:', clientId, data);
|
||||||
|
|
||||||
|
if (clients[clientId]) {
|
||||||
|
clients[clientId].command_status = {
|
||||||
|
status: data.status || 'N/A',
|
||||||
|
program_status: data.program_status || 'N/A'
|
||||||
|
};
|
||||||
|
if (data.output) {
|
||||||
|
clients[clientId].command_status.output = data.output;
|
||||||
|
}
|
||||||
|
if (data.error) {
|
||||||
|
clients[clientId].command_status.error = data.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeCommand(action) {
|
||||||
|
const selectedClients = getSelectedClients();
|
||||||
|
if (selectedClients.length === 0) {
|
||||||
|
alert('请选择至少一个客户端');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let command = action;
|
||||||
|
let params = {
|
||||||
|
updateUrl: document.getElementById('updateUrl').value,
|
||||||
|
programName: document.getElementById('programName').value,
|
||||||
|
programPath: document.getElementById('programPath').value,
|
||||||
|
commandParams: document.getElementById('commandParams').value
|
||||||
|
};
|
||||||
|
|
||||||
|
if (action === 'manual') {
|
||||||
|
params.manualCommand = document.getElementById('manualCommand').value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空之前的结果
|
||||||
|
const resultsDiv = document.getElementById('commandResults');
|
||||||
|
while (resultsDiv.children.length > 1) { // 保留标题 <h3>
|
||||||
|
resultsDiv.removeChild(resultsDiv.lastChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/execute_command', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_ids: selectedClients,
|
||||||
|
command: command,
|
||||||
|
params: params
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('Command sent:', data);
|
||||||
|
alert(`命令已发送到 ${data.affected_clients} 个客户端。请等待结果...`);
|
||||||
|
selectedClients.forEach(clientId => {
|
||||||
|
if (clients[clientId]) {
|
||||||
|
updateClientStatus(clientId, { status: '执行中' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateClientTable();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error sending command:', error);
|
||||||
|
alert('发送命令时出错');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSettings() {
|
||||||
|
const heartbeatTime = document.getElementById('heartbeatTime').value;
|
||||||
|
const reportTime = document.getElementById('reportTime').value;
|
||||||
|
fetch('/api/update_settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ heartbeatTime, reportTime }),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('Settings updated:', data);
|
||||||
|
alert('设置已更新');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNewGroup() {
|
||||||
|
const newGroup = document.getElementById('newGroupName').value;
|
||||||
|
if (newGroup && !groups.includes(newGroup)) {
|
||||||
|
fetch('/api/add_group', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ group: newGroup }),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('Group added:', data);
|
||||||
|
alert(`新分组 "${newGroup}" 已添加`);
|
||||||
|
fetchClients();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert("分组名称无效或已存在");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteGroup(group) {
|
||||||
|
if (group === 'default' || group === '离线') {
|
||||||
|
alert('无法删除"默认"或"离线"分组');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (confirm(`确定要删除分组 "${group}" 吗?`)) {
|
||||||
|
fetch('/api/delete_group', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ group: group }),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('Group deleted:', data);
|
||||||
|
alert(`分组 "${group}" 已删除`);
|
||||||
|
fetchClients();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeClientGroup(clientId) {
|
||||||
|
const newGroup = prompt(`请为客户端 ${clientId} 输入新的分组名称:`);
|
||||||
|
if (newGroup) {
|
||||||
|
fetch('/api/update_client_group', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ client_ids: [clientId], group: newGroup }),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('Client group updated:', data);
|
||||||
|
if (data.status === "Client group updated") {
|
||||||
|
alert(`客户端 ${clientId} 已更新到分组 "${newGroup}"`);
|
||||||
|
fetchClients();
|
||||||
|
} else {
|
||||||
|
alert('更新分组失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayCommandResult(data) {
|
||||||
|
console.log('Displaying command result:', data);
|
||||||
|
|
||||||
|
const resultsDiv = document.getElementById('commandResults');
|
||||||
|
const resultItem = document.createElement('div');
|
||||||
|
resultItem.className = 'result-item';
|
||||||
|
|
||||||
|
// 创建一个 Date 对象并格式化为北京时间
|
||||||
|
const timestamp = new Date(data.timestamp);
|
||||||
|
const localTime = timestamp.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
||||||
|
|
||||||
|
resultItem.innerHTML = `
|
||||||
|
<strong>客户端 ID:</strong> ${data.client_id}<br>
|
||||||
|
<strong>命令:</strong> ${data.command || 'N/A'}<br>
|
||||||
|
<strong>状态:</strong> ${data.status || 'N/A'}<br>
|
||||||
|
<strong>输出:</strong> <pre>${data.output || '无输出'}</pre>
|
||||||
|
<strong>错误:</strong> <pre>${data.error || '无错误'}</pre>
|
||||||
|
<strong>程序状态:</strong> ${data.program_status || 'N/A'}<br>
|
||||||
|
<strong>时间:</strong> ${localTime}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 添加新结果到底部
|
||||||
|
resultsDiv.appendChild(resultItem);
|
||||||
|
|
||||||
|
// 保持最多显示10条结果
|
||||||
|
while (resultsDiv.children.length > 11) { // 11 = 标题 + 10条结果
|
||||||
|
resultsDiv.removeChild(resultsDiv.children[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
resultsDiv.scrollTop = resultsDiv.scrollHeight;
|
||||||
|
|
||||||
|
updateClientStatus(data.client_id, {
|
||||||
|
status: data.status,
|
||||||
|
program_status: data.program_status
|
||||||
|
});
|
||||||
|
|
||||||
|
updateClientTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showManualCommandModal() {
|
||||||
|
if (!manualCommandModal) {
|
||||||
|
manualCommandModal = new bootstrap.Modal(document.getElementById('manualCommandModal'));
|
||||||
|
}
|
||||||
|
manualCommandModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeManualCommand() {
|
||||||
|
const command = document.getElementById('manualCommand').value;
|
||||||
|
if (command) {
|
||||||
|
executeCommand('manual');
|
||||||
|
manualCommandModal.hide();
|
||||||
|
} else {
|
||||||
|
alert('请输入命令');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfig() {
|
||||||
|
const configName = document.getElementById('configName').value;
|
||||||
|
if (!configName) {
|
||||||
|
alert('请输入配置名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const config = {
|
||||||
|
updateUrl: document.getElementById('updateUrl').value,
|
||||||
|
programName: document.getElementById('programName').value,
|
||||||
|
programPath: document.getElementById('programPath').value,
|
||||||
|
commandParams: document.getElementById('commandParams').value
|
||||||
|
};
|
||||||
|
fetch('/api/save_config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name: configName, config: config }),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
alert('配置已保存');
|
||||||
|
loadConfig();
|
||||||
|
} else {
|
||||||
|
alert('保存配置失败: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('保存配置时发生错误');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadConfig() {
|
||||||
|
fetch('/api/get_configs', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(configs => {
|
||||||
|
updateConfigSelector(configs);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('加载配置时发生错误');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConfigSelector(configs) {
|
||||||
|
const selector = document.getElementById('configSelector');
|
||||||
|
selector.innerHTML = '<option value="">-- 选择配置 --</option>';
|
||||||
|
for (const [name, _] of Object.entries(configs)) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = name;
|
||||||
|
option.textContent = name;
|
||||||
|
selector.appendChild(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSelectedConfig() {
|
||||||
|
const configName = document.getElementById('configSelector').value;
|
||||||
|
if (!configName) {
|
||||||
|
// 如果没有选择配置,清空所有字段
|
||||||
|
clearConfigFields();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch('/api/get_configs', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(configs => {
|
||||||
|
const config = configs[configName];
|
||||||
|
if (config) {
|
||||||
|
document.getElementById('updateUrl').value = config.updateUrl || '';
|
||||||
|
document.getElementById('programName').value = config.programName || '';
|
||||||
|
document.getElementById('programPath').value = config.programPath || '';
|
||||||
|
document.getElementById('commandParams').value = config.commandParams || '';
|
||||||
|
document.getElementById('configName').value = configName;
|
||||||
|
} else {
|
||||||
|
throw new Error('Selected configuration not found');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading configuration:', error);
|
||||||
|
alert('加载配置时发生错误: ' + error.message);
|
||||||
|
clearConfigFields();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearConfigFields() {
|
||||||
|
document.getElementById('updateUrl').value = '';
|
||||||
|
document.getElementById('programName').value = '';
|
||||||
|
document.getElementById('programPath').value = '';
|
||||||
|
document.getElementById('commandParams').value = '';
|
||||||
|
document.getElementById('configName').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteConfig() {
|
||||||
|
const configName = document.getElementById('configSelector').value;
|
||||||
|
if (!configName) {
|
||||||
|
alert('请选择要删除的配置');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (confirm(`确定要删除配置 "${configName}" 吗?`)) {
|
||||||
|
fetch('/api/delete_config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name: configName }),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
alert('配置已删除');
|
||||||
|
loadConfig();
|
||||||
|
// 清空输入框
|
||||||
|
clearConfigFields();
|
||||||
|
} else {
|
||||||
|
alert('删除配置失败: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('删除配置时发生错误');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelectedClients() {
|
||||||
|
const selectedClients = getSelectedClients();
|
||||||
|
if (selectedClients.length === 0) {
|
||||||
|
alert('请选择至少一个客户端');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (confirm(`确定要删除选中的 ${selectedClients.length} 个客户端吗?`)) {
|
||||||
|
fetch('/api/delete_clients', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ client_ids: selectedClients }),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
alert(data.message);
|
||||||
|
selectedClients.forEach(clientId => {
|
||||||
|
delete clients[clientId];
|
||||||
|
});
|
||||||
|
selectedClients.clear();
|
||||||
|
updateClientTable();
|
||||||
|
} else {
|
||||||
|
alert('删除客户端失败: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('删除客户端时发生错误');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeBulkClientGroup() {
|
||||||
|
const selectedClients = getSelectedClients();
|
||||||
|
if (selectedClients.length === 0) {
|
||||||
|
alert('请选择至少一个客户端');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newGroup = prompt(`请为选中的 ${selectedClients.length} 个客户端输入新的分组名称:`);
|
||||||
|
if (newGroup) {
|
||||||
|
fetch('/api/update_client_group', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ client_ids: selectedClients, group: newGroup }),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('Client group updated:', data);
|
||||||
|
if (data.status === "Client group updated") {
|
||||||
|
alert(`${selectedClients.length} 个客户端已更新到分组 "${newGroup}"`);
|
||||||
|
fetchClients();
|
||||||
|
} else {
|
||||||
|
alert('更新分组失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchCommandResults() {
|
||||||
|
console.log('Fetching command results...');
|
||||||
|
fetch('/api/command_results', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(results => {
|
||||||
|
console.log('Received command results:', results);
|
||||||
|
// 清空现有结果
|
||||||
|
const resultsDiv = document.getElementById('commandResults');
|
||||||
|
while (resultsDiv.children.length > 1) { // 保留标题 <h3>
|
||||||
|
resultsDiv.removeChild(resultsDiv.lastChild);
|
||||||
|
}
|
||||||
|
// 按时间正序排列结果
|
||||||
|
results.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
||||||
|
// 显示最新的结果(最多10条)
|
||||||
|
results.slice(-10).forEach(result => {
|
||||||
|
const resultItem = document.createElement('div');
|
||||||
|
resultItem.className = 'result-item';
|
||||||
|
// 创建一个 Date 对象并格式化为北京时间
|
||||||
|
const timestamp = new Date(result.timestamp);
|
||||||
|
const localTime = timestamp.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
||||||
|
resultItem.innerHTML = `
|
||||||
|
<strong>客户端 ID:</strong> ${result.client_id}<br>
|
||||||
|
<strong>命令:</strong> ${result.command || 'N/A'}<br>
|
||||||
|
<strong>状态:</strong> ${result.status || 'N/A'}<br>
|
||||||
|
<strong>输出:</strong> <pre>${result.output || '无输出'}</pre>
|
||||||
|
<strong>错误:</strong> <pre>${result.error || '无错误'}</pre>
|
||||||
|
<strong>程序状态:</strong> ${result.program_status || 'N/A'}<br>
|
||||||
|
<strong>时间:</strong> ${localTime}
|
||||||
|
`;
|
||||||
|
resultsDiv.appendChild(resultItem);
|
||||||
|
});
|
||||||
|
// 滚动到底部
|
||||||
|
resultsDiv.scrollTop = resultsDiv.scrollHeight;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching command results:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseDown(e) {
|
||||||
|
if (e.target.closest('#clientTable')) {
|
||||||
|
isMouseDown = true;
|
||||||
|
startX = e.clientX;
|
||||||
|
startY = e.clientY;
|
||||||
|
selectionBox.style.left = startX + 'px';
|
||||||
|
selectionBox.style.top = startY + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(e) {
|
||||||
|
if (!isMouseDown) return;
|
||||||
|
|
||||||
|
const currentX = e.clientX;
|
||||||
|
const currentY = e.clientY;
|
||||||
|
|
||||||
|
const boxLeft = Math.min(startX, currentX);
|
||||||
|
const boxTop = Math.min(startY, currentY);
|
||||||
|
const boxWidth = Math.abs(currentX - startX);
|
||||||
|
const boxHeight = Math.abs(currentY - startY);
|
||||||
|
|
||||||
|
selectionBox.style.display = 'block';
|
||||||
|
selectionBox.style.left = boxLeft + 'px';
|
||||||
|
selectionBox.style.top = boxTop + 'px';
|
||||||
|
selectionBox.style.width = boxWidth + 'px';
|
||||||
|
selectionBox.style.height = boxHeight + 'px';
|
||||||
|
|
||||||
|
const tableRect = document.getElementById('clientTable').getBoundingClientRect();
|
||||||
|
const rows = document.querySelectorAll('#clientTable tbody tr');
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const rowRect = row.getBoundingClientRect();
|
||||||
|
const isIntersecting = !(rowRect.right < boxLeft ||
|
||||||
|
rowRect.left > boxLeft + boxWidth ||
|
||||||
|
rowRect.bottom < boxTop ||
|
||||||
|
rowRect.top > boxTop + boxHeight);
|
||||||
|
|
||||||
|
if (isIntersecting) {
|
||||||
|
const clientId = row.dataset.clientId;
|
||||||
|
toggleClientSelection(clientId, row, true);
|
||||||
|
} else if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
||||||
|
const clientId = row.dataset.clientId;
|
||||||
|
toggleClientSelection(clientId, row, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp() {
|
||||||
|
isMouseDown = false;
|
||||||
|
selectionBox.style.display = 'none';
|
||||||
|
selectionBox.style.width = '0';
|
||||||
|
selectionBox.style.height = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRowClick(e) {
|
||||||
|
const row = e.target.closest('tr');
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
const clientId = row.dataset.clientId;
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
const rows = Array.from(document.querySelectorAll('#clientTable tbody tr'));
|
||||||
|
const lastSelectedIndex = rows.findIndex(r => r.classList.contains('selected'));
|
||||||
|
const currentIndex = rows.indexOf(row);
|
||||||
|
|
||||||
|
const start = Math.min(lastSelectedIndex, currentIndex);
|
||||||
|
const end = Math.max(lastSelectedIndex, currentIndex);
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
const r = rows[i];
|
||||||
|
toggleClientSelection(r.dataset.clientId, r, true);
|
||||||
|
}
|
||||||
|
} else if (e.ctrlKey || e.metaKey) {
|
||||||
|
toggleClientSelection(clientId, row, true);
|
||||||
|
} else {
|
||||||
|
selectedClients.clear();
|
||||||
|
document.querySelectorAll('#clientTable tbody tr').forEach(r => r.classList.remove('selected'));
|
||||||
|
toggleClientSelection(clientId, row, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
console.log('Initializing...');
|
||||||
|
fetchClients();
|
||||||
|
loadConfig();
|
||||||
|
fetchCommandResults();
|
||||||
|
setInterval(fetchClients, 10000);
|
||||||
|
setInterval(fetchCommandResults, 5000);
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleMouseDown);
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
document.getElementById('clientTable').addEventListener('click', handleRowClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', init);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user