297 lines
14 KiB
HTML
297 lines
14 KiB
HTML
<div class="mb-6 flex justify-between items-center">
|
||
<div class="flex items-center">
|
||
<a href="/admin/robots" class="text-gray-500 hover:text-gray-700 mr-4 transition-colors">
|
||
<i class="fas fa-arrow-left"></i>
|
||
</a>
|
||
<h1 class="text-2xl font-semibold text-gray-800">{{.Robot.Nickname}}</h1>
|
||
<span class="ml-3 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||
{{if eq .Robot.Status "online"}}
|
||
bg-emerald-100 text-emerald-800
|
||
{{else if eq .Robot.Status "error"}}
|
||
bg-red-100 text-red-800
|
||
{{else}}
|
||
bg-gray-100 text-gray-800
|
||
{{end}}">
|
||
{{if eq .Robot.Status "online"}}在线{{else if eq .Robot.Status "error"}}错误{{else}}离线{{end}}
|
||
</span>
|
||
</div>
|
||
<div>
|
||
<!-- 修改删除按钮,使用confirmDialog而非直接调用deleteRobot函数 -->
|
||
<button onclick="confirmDelete({{.Robot.ID}}, '{{.Robot.Nickname}}')" class="inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 transition-colors">
|
||
<i class="fas fa-trash mr-2"></i> 删除
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||
<!-- 左侧:机器人信息 -->
|
||
<div class="lg:col-span-1">
|
||
<div class="clean-card overflow-hidden">
|
||
<div class="pattern-dots h-24 relative flex items-center justify-center bg-gray-50 border-b border-gray-100">
|
||
<div class="absolute w-20 h-20 rounded-full bg-white shadow-sm border border-gray-100 flex items-center justify-center overflow-hidden">
|
||
{{if .Robot.Avatar}}
|
||
<img src="{{.Robot.Avatar}}" alt="WeChat Avatar" class="w-full h-full object-cover">
|
||
{{else}}
|
||
<i class="fas fa-robot text-3xl text-gray-500"></i>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pt-12 pb-6 px-6 text-center">
|
||
<h3 class="text-xl font-semibold text-gray-800">{{.Robot.Nickname}}</h3>
|
||
{{if .Robot.WechatID}}
|
||
<p class="text-gray-500">{{.Robot.WechatID}}</p>
|
||
{{else}}
|
||
<p class="text-gray-400 italic text-sm">未登录</p>
|
||
{{end}}
|
||
</div>
|
||
|
||
<!-- 机器人详情信息 -->
|
||
<div class="border-t border-gray-100">
|
||
<div class="p-6 space-y-4">
|
||
<div class="flex justify-between text-sm">
|
||
<span class="text-gray-600">创建时间</span>
|
||
<span class="text-gray-800">{{.Robot.CreatedAt.Format "2006-01-02 15:04:05"}}</span>
|
||
</div>
|
||
<div class="flex justify-between text-sm">
|
||
<span class="text-gray-600">状态</span>
|
||
<span class="{{if eq .Robot.Status "online"}}text-emerald-600{{else if eq .Robot.Status "error"}}text-red-600{{else}}text-gray-600{{end}}">
|
||
{{if eq .Robot.Status "online"}}在线{{else if eq .Robot.Status "error"}}错误{{else}}离线{{end}}
|
||
</span>
|
||
</div>
|
||
<div class="flex justify-between text-sm">
|
||
<span class="text-gray-600">容器ID</span>
|
||
<span class="text-gray-800 font-mono text-xs" title="{{.Robot.ContainerID}}">
|
||
{{if .Robot.ContainerID}}{{slice .Robot.ContainerID 0 12}}{{else}}未知{{end}}
|
||
</span>
|
||
</div>
|
||
{{if .Robot.LastLoginAt}}
|
||
<div class="flex justify-between text-sm">
|
||
<span class="text-gray-600">最后登录</span>
|
||
<span class="text-gray-800">{{.Robot.LastLoginAt.Format "2006-01-02 15:04:05"}}</span>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="bg-gray-50 border-t border-gray-100 p-6 flex space-x-3">
|
||
{{if eq .Robot.Status "online"}}
|
||
<form action="/admin/robots/{{.Robot.ID}}/logout" method="POST" class="w-full">
|
||
<button type="submit" class="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-800 hover:bg-gray-700 focus:outline-none transition-colors">
|
||
<i class="fas fa-sign-out-alt mr-2"></i> 退出登录
|
||
</button>
|
||
</form>
|
||
{{else}}
|
||
<a href="/admin/robots/{{.Robot.ID}}/login" class="w-full flex items-center justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none transition-colors">
|
||
<i class="fas fa-qrcode mr-2"></i> 登录
|
||
</a>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:容器日志和状态信息 -->
|
||
<div class="lg:col-span-2 space-y-6">
|
||
<!-- 容器状态 -->
|
||
<div class="clean-card overflow-hidden">
|
||
<div class="flex justify-between items-center px-6 py-4 border-b border-gray-100">
|
||
<h3 class="font-medium text-gray-800">容器状态</h3>
|
||
<button id="refresh-btn" class="inline-flex items-center justify-center p-1.5 rounded-full text-gray-600 hover:bg-gray-100 transition-colors">
|
||
<i class="fas fa-sync-alt"></i>
|
||
</button>
|
||
</div>
|
||
<div class="p-6 grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||
<div class="space-y-2">
|
||
<p class="text-sm text-gray-500">CPU 使用率</p>
|
||
<div class="flex items-center">
|
||
<div class="w-full bg-gray-100 rounded-full h-2 mr-3">
|
||
<div id="cpu-bar" class="bg-blue-500 h-2 rounded-full" style="width: 0%"></div>
|
||
</div>
|
||
<span id="cpu-usage" class="text-sm font-medium text-gray-700 min-w-[40px]">0%</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<p class="text-sm text-gray-500">内存使用率</p>
|
||
<div class="flex items-center">
|
||
<div class="w-full bg-gray-100 rounded-full h-2 mr-3">
|
||
<div id="memory-bar" class="bg-green-500 h-2 rounded-full" style="width: 0%"></div>
|
||
</div>
|
||
<span id="memory-usage" class="text-sm font-medium text-gray-700 min-w-[40px]">0%</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<p class="text-sm text-gray-500 mb-1">网络接收</p>
|
||
<p id="network-rx" class="text-lg font-medium text-gray-800">0 KB/s</p>
|
||
</div>
|
||
|
||
<div>
|
||
<p class="text-sm text-gray-500 mb-1">网络发送</p>
|
||
<p id="network-tx" class="text-lg font-medium text-gray-800">0 KB/s</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 容器日志 -->
|
||
<div class="clean-card overflow-hidden">
|
||
<div class="flex justify-between items-center px-6 py-4 border-b border-gray-100">
|
||
<div class="flex items-center">
|
||
<h3 class="font-medium text-gray-800">容器日志</h3>
|
||
<div id="logs-loading" class="ml-3 hidden">
|
||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-500"></div>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center space-x-3">
|
||
<select id="log-lines" class="text-sm border border-gray-200 rounded-md py-1 pl-2 pr-8 bg-white focus:ring-1 focus:ring-gray-200 focus:border-gray-300">
|
||
<option value="50">50行</option>
|
||
<option value="100" selected>100行</option>
|
||
<option value="200">200行</option>
|
||
<option value="500">500行</option>
|
||
</select>
|
||
<button id="reload-logs" class="inline-flex items-center justify-center p-1.5 rounded-full text-gray-600 hover:bg-gray-100 transition-colors">
|
||
<i class="fas fa-sync-alt"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="p-6">
|
||
<pre id="container-logs" class="text-xs font-mono bg-gray-50 border border-gray-100 rounded-md p-4 overflow-auto h-80 text-gray-700">正在加载日志...</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const containerLogs = document.getElementById('container-logs');
|
||
const logsLoading = document.getElementById('logs-loading');
|
||
const logLines = document.getElementById('log-lines');
|
||
const reloadLogs = document.getElementById('reload-logs');
|
||
const refreshBtn = document.getElementById('refresh-btn');
|
||
|
||
// 容器ID可能长度不一致,确保使用完整ID
|
||
const containerId = "{{.Robot.ContainerID}}";
|
||
|
||
// 加载日志
|
||
function loadLogs() {
|
||
logsLoading.style.display = 'flex';
|
||
|
||
fetch(`/api/v1/containers/${containerId}/logs?lines=${logLines.value}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success && data.logs) {
|
||
containerLogs.textContent = data.logs;
|
||
containerLogs.scrollTop = containerLogs.scrollHeight;
|
||
} else {
|
||
containerLogs.textContent = '无法获取日志: ' + (data.message || '未知错误');
|
||
}
|
||
logsLoading.style.display = 'none';
|
||
})
|
||
.catch(error => {
|
||
console.error('Error fetching logs:', error);
|
||
containerLogs.textContent = '加载日志时发生错误: ' + error.message;
|
||
logsLoading.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
// 加载状态信息
|
||
function loadStats() {
|
||
fetch(`/api/v1/containers/${containerId}/stats`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// 更新CPU使用率
|
||
const cpuPercent = (data.stats.cpu_percent || 0).toFixed(2);
|
||
document.getElementById('cpu-usage').textContent = `${cpuPercent}%`;
|
||
document.getElementById('cpu-bar').style.width = `${Math.min(cpuPercent, 100)}%`;
|
||
|
||
// 更新内存使用率
|
||
const memoryUsage = data.stats.memory_usage || 0;
|
||
const memoryLimit = data.stats.memory_limit || 1;
|
||
const memoryPercent = ((memoryUsage / memoryLimit) * 100).toFixed(2);
|
||
document.getElementById('memory-usage').textContent = `${memoryPercent}%`;
|
||
document.getElementById('memory-bar').style.width = `${Math.min(memoryPercent, 100)}%`;
|
||
|
||
// 更新网络信息
|
||
const networkRx = formatBytes(data.stats.network_rx || 0) + '/s';
|
||
const networkTx = formatBytes(data.stats.network_tx || 0) + '/s';
|
||
document.getElementById('network-rx').textContent = networkRx;
|
||
document.getElementById('network-tx').textContent = networkTx;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('加载状态信息失败:', error);
|
||
});
|
||
}
|
||
|
||
// 格式化字节数
|
||
function formatBytes(bytes, decimals = 2) {
|
||
if (bytes === 0) return '0 B';
|
||
|
||
const k = 1024;
|
||
const dm = decimals < 0 ? 0 : decimals;
|
||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||
}
|
||
|
||
// 初始加载日志和状态
|
||
loadLogs();
|
||
loadStats();
|
||
|
||
// 设置事件监听器
|
||
reloadLogs.addEventListener('click', loadLogs);
|
||
logLines.addEventListener('change', loadLogs);
|
||
refreshBtn.addEventListener('click', function() {
|
||
this.classList.add('animate-spin');
|
||
loadStats();
|
||
setTimeout(() => {
|
||
this.classList.remove('animate-spin');
|
||
}, 1000);
|
||
});
|
||
|
||
// 定期更新状态
|
||
setInterval(loadStats, 5000); // 每5秒更新一次
|
||
});
|
||
|
||
// 确认删除函数
|
||
async function confirmDelete(id, name) {
|
||
if (typeof confirmDialog === 'function') {
|
||
const confirmed = await confirmDialog(
|
||
`确定要删除机器人"${name}"吗?此操作将永久删除容器及相关数据,无法恢复!`,
|
||
{ type: 'danger', title: '删除机器人' }
|
||
);
|
||
|
||
if (confirmed) {
|
||
deleteRobot(id);
|
||
}
|
||
} else {
|
||
// 降级方案:使用原生confirm
|
||
if (confirm(`确定要删除机器人"${name}"吗?此操作不可恢复!`)) {
|
||
deleteRobot(id);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 删除机器人
|
||
function deleteRobot(id) {
|
||
fetch(`/admin/robots/${id}`, {
|
||
method: 'DELETE',
|
||
})
|
||
.then(response => {
|
||
if (response.redirected) {
|
||
window.location.href = response.url;
|
||
} else {
|
||
window.location.href = '/admin/robots';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('删除机器人失败');
|
||
});
|
||
}
|
||
</script>
|