Yolov/templates/task_management.html

1749 lines
60 KiB
HTML
Raw Normal View History

2025-12-11 13:41:07 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多任务视频流检测管理平台</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/Chart.js/4.5.0/chart.js"></script>
<style>
:root {
--primary-color: #2c3e50;
--secondary-color: #3498db;
--success-color: #27ae60;
--warning-color: #f39c12;
--danger-color: #e74c3c;
--light-color: #ecf0f1;
--dark-color: #2c3e50;
--gray-color: #95a5a6;
--border-radius: 8px;
--box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* 头部样式 */
header {
background: linear-gradient(135deg, var(--primary-color), #1a252f);
color: white;
padding: 20px;
border-radius: var(--border-radius);
margin-bottom: 25px;
box-shadow: var(--box-shadow);
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: 15px;
}
.logo i {
font-size: 2.5rem;
color: var(--secondary-color);
}
.logo h1 {
font-size: 1.8rem;
font-weight: 600;
}
.logo span {
color: var(--secondary-color);
}
.connection-status {
display: flex;
align-items: center;
gap: 10px;
background: rgba(255, 255, 255, 0.1);
padding: 10px 15px;
border-radius: var(--border-radius);
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: var(--success-color);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
/* 系统状态卡片 */
.system-status-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.status-card {
background: white;
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--box-shadow);
transition: var(--transition);
border-left: 4px solid var(--secondary-color);
}
.status-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.status-card.cpu {
border-left-color: #e74c3c;
}
.status-card.memory {
border-left-color: #3498db;
}
.status-card.disk {
border-left-color: #9b59b6;
}
.status-card.gpu {
border-left-color: #27ae60;
}
.status-card.tasks {
border-left-color: #f39c12;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.card-header h3 {
font-size: 1.2rem;
font-weight: 600;
color: var(--primary-color);
}
.card-icon {
font-size: 1.8rem;
color: var(--gray-color);
}
.card-value {
font-size: 2rem;
font-weight: 700;
color: var(--dark-color);
margin-bottom: 5px;
}
.progress-bar {
height: 8px;
background-color: #ecf0f1;
border-radius: 4px;
overflow: hidden;
margin-top: 10px;
}
.progress {
height: 100%;
background: linear-gradient(90deg, var(--secondary-color), #2980b9);
border-radius: 4px;
transition: width 0.5s ease;
}
.progress.cpu {
background: linear-gradient(90deg, #e74c3c, #c0392b);
}
.progress.memory {
background: linear-gradient(90deg, #3498db, #2980b9);
}
.progress.disk {
background: linear-gradient(90deg, #9b59b6, #8e44ad);
}
/* 创建任务表单 */
.task-form-container {
background: white;
border-radius: var(--border-radius);
padding: 25px;
margin-bottom: 30px;
box-shadow: var(--box-shadow);
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.form-header h2 {
color: var(--primary-color);
font-size: 1.5rem;
}
.form-toggle-btn {
background: var(--secondary-color);
color: white;
border: none;
padding: 8px 15px;
border-radius: var(--border-radius);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: var(--transition);
}
.form-toggle-btn:hover {
background: #2980b9;
}
.task-form {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
margin-bottom: 8px;
font-weight: 600;
color: var(--primary-color);
display: flex;
align-items: center;
gap: 8px;
}
.form-group label i {
color: var(--secondary-color);
}
.form-group input,
.form-group select {
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: var(--border-radius);
font-size: 1rem;
transition: var(--transition);
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--secondary-color);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}
.form-actions {
grid-column: 1 / -1;
display: flex;
justify-content: flex-end;
gap: 15px;
margin-top: 20px;
}
.btn {
padding: 12px 25px;
border: none;
border-radius: var(--border-radius);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background: var(--secondary-color);
color: white;
}
.btn-primary:hover {
background: #2980b9;
}
.btn-secondary {
background: #95a5a6;
color: white;
}
.btn-secondary:hover {
background: #7f8c8d;
}
.btn-success {
background: var(--success-color);
color: white;
}
.btn-success:hover {
background: #219653;
}
.btn-danger {
background: var(--danger-color);
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
.btn-warning {
background: var(--warning-color);
color: white;
}
.btn-warning:hover {
background: #e67e22;
}
/* 任务列表 */
.tasks-container {
background: white;
border-radius: var(--border-radius);
padding: 25px;
box-shadow: var(--box-shadow);
}
.tasks-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.tasks-header h2 {
color: var(--primary-color);
font-size: 1.5rem;
}
.tasks-count {
background: var(--light-color);
padding: 8px 15px;
border-radius: var(--border-radius);
font-weight: 600;
color: var(--primary-color);
}
.tasks-count span {
color: var(--secondary-color);
}
.tasks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 20px;
}
.task-card {
background: white;
border-radius: var(--border-radius);
border: 1px solid #eee;
overflow: hidden;
transition: var(--transition);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.task-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.task-card.running {
border-left: 4px solid var(--success-color);
}
.task-card.starting {
border-left: 4px solid var(--warning-color);
}
.task-card.stopped {
border-left: 4px solid var(--danger-color);
}
.task-card.creating {
border-left: 4px solid var(--secondary-color);
}
.task-header {
background: #f8f9fa;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
}
.task-title {
display: flex;
align-items: center;
gap: 10px;
}
.task-icon {
font-size: 1.5rem;
color: var(--secondary-color);
}
.task-name {
font-weight: 600;
color: var(--primary-color);
font-size: 1.1rem;
}
.task-status {
padding: 5px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
}
.status-running {
background: rgba(39, 174, 96, 0.1);
color: var(--success-color);
}
.status-starting {
background: rgba(243, 156, 18, 0.1);
color: var(--warning-color);
}
.status-stopped {
background: rgba(231, 76, 60, 0.1);
color: var(--danger-color);
}
.status-creating {
background: rgba(52, 152, 219, 0.1);
color: var(--secondary-color);
}
.task-body {
padding: 20px;
}
.task-info {
margin-bottom: 20px;
}
.info-row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px dashed #eee;
}
.info-label {
color: var(--gray-color);
font-weight: 500;
}
.info-value {
color: var(--dark-color);
font-weight: 600;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-stats {
background: #f8f9fa;
border-radius: var(--border-radius);
padding: 15px;
margin-bottom: 20px;
}
.stats-row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.stat {
text-align: center;
flex: 1;
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary-color);
}
.stat-label {
font-size: 0.85rem;
color: var(--gray-color);
margin-top: 5px;
}
.task-actions {
display: flex;
gap: 10px;
}
.task-actions .btn {
flex: 1;
padding: 10px;
font-size: 0.9rem;
}
/* 实时检测面板 */
.realtime-panel {
margin-top: 30px;
background: white;
border-radius: var(--border-radius);
padding: 25px;
box-shadow: var(--box-shadow);
}
.detections-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.detection-card {
background: #f8f9fa;
border-radius: var(--border-radius);
padding: 15px;
border-left: 4px solid var(--secondary-color);
}
.detection-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.detection-class {
font-weight: 600;
color: var(--primary-color);
}
.detection-confidence {
background: var(--secondary-color);
color: white;
padding: 3px 10px;
border-radius: 20px;
font-size: 0.85rem;
}
.detection-box {
font-size: 0.9rem;
color: var(--gray-color);
}
/* 响应式设计 */
@media (max-width: 768px) {
.tasks-grid {
grid-template-columns: 1fr;
}
.task-form {
grid-template-columns: 1fr;
}
.system-status-cards {
grid-template-columns: 1fr;
}
header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.form-actions {
flex-direction: column;
}
.task-actions {
flex-direction: column;
}
}
/* 模态框 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
border-radius: var(--border-radius);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
animation: modalFadeIn 0.3s ease;
}
@keyframes modalFadeIn {
from { opacity: 0; transform: translateY(-50px); }
to { opacity: 1; transform: translateY(0); }
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
color: var(--primary-color);
font-size: 1.3rem;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
color: var(--gray-color);
cursor: pointer;
transition: var(--transition);
}
.modal-close:hover {
color: var(--danger-color);
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 通知 */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: var(--border-radius);
color: white;
font-weight: 600;
box-shadow: var(--box-shadow);
z-index: 9999;
animation: slideIn 0.3s ease, slideOut 0.3s ease 2.7s;
max-width: 400px;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.notification.success {
background: var(--success-color);
border-left: 4px solid #219653;
}
.notification.error {
background: var(--danger-color);
border-left: 4px solid #c0392b;
}
.notification.warning {
background: var(--warning-color);
border-left: 4px solid #e67e22;
}
.notification.info {
background: var(--secondary-color);
border-left: 4px solid #2980b9;
}
/* 加载动画 */
.loader {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 折叠面板 */
.collapse-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.collapse-content.show {
max-height: 2000px;
}
/* 选项卡 */
.tabs {
display: flex;
border-bottom: 1px solid #eee;
margin-bottom: 20px;
}
.tab {
padding: 12px 20px;
background: none;
border: none;
font-size: 1rem;
font-weight: 600;
color: var(--gray-color);
cursor: pointer;
transition: var(--transition);
border-bottom: 3px solid transparent;
}
.tab.active {
color: var(--secondary-color);
border-bottom-color: var(--secondary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 50px 20px;
color: var(--gray-color);
}
.empty-state i {
font-size: 3rem;
margin-bottom: 15px;
color: #ddd;
}
.empty-state h3 {
font-size: 1.3rem;
margin-bottom: 10px;
color: var(--primary-color);
}
</style>
</head>
<body>
<div class="container">
<!-- 头部 -->
<header>
<div class="logo">
<i class="fas fa-video"></i>
<div>
<h1>多任务<span>视频流</span>检测管理平台</h1>
<p>实时监控和管理多个RTMP视频流检测任务</p>
</div>
</div>
<div class="connection-status">
<div class="status-dot" id="connectionStatus"></div>
<span id="connectionText">连接中...</span>
</div>
</header>
<!-- 系统状态卡片 -->
<div class="system-status-cards">
<div class="status-card cpu">
<div class="card-header">
<h3>CPU使用率</h3>
<i class="fas fa-microchip card-icon"></i>
</div>
<div class="card-value" id="cpuPercent">0%</div>
<div class="progress-bar">
<div class="progress cpu" id="cpuProgress" style="width: 0%"></div>
</div>
</div>
<div class="status-card memory">
<div class="card-header">
<h3>内存使用率</h3>
<i class="fas fa-memory card-icon"></i>
</div>
<div class="card-value" id="memoryPercent">0%</div>
<div class="progress-bar">
<div class="progress memory" id="memoryProgress" style="width: 0%"></div>
</div>
</div>
<div class="status-card tasks">
<div class="card-header">
<h3>任务状态</h3>
<i class="fas fa-tasks card-icon"></i>
</div>
<div class="card-value" id="tasksCount">0/0</div>
<p>活动任务: <span id="activeTasks">0</span> / 总任务: <span id="totalTasks">0</span></p>
</div>
<div class="status-card gpu">
<div class="card-header">
<h3>GPU状态</h3>
<i class="fas fa-microchip card-icon"></i>
</div>
<div class="card-value" id="gpuStatus">不可用</div>
<p id="gpuInfo">等待数据...</p>
</div>
</div>
<!-- 创建任务表单 -->
<div class="task-form-container">
<div class="form-header">
<h2><i class="fas fa-plus-circle"></i> 创建新检测任务</h2>
<button class="form-toggle-btn" id="toggleFormBtn">
<i class="fas fa-chevron-down"></i> 展开表单
</button>
</div>
<div class="collapse-content" id="taskFormContent">
<form class="task-form" id="createTaskForm">
<div class="form-group">
<label for="rtmpUrl"><i class="fas fa-signal"></i> RTMP视频流地址</label>
<input type="text" id="rtmpUrl" name="rtmp_url" required
placeholder="例如: rtmp://localhost:1935/live/stream1"
value="rtmp://localhost:1935/live/14">
</div>
<div class="form-group">
<label for="pushUrl"><i class="fas fa-broadcast-tower"></i> 推流输出地址 (可选)</label>
<input type="text" id="pushUrl" name="push_url"
placeholder="例如: rtmp://localhost:1935/live/output1"
value="rtmp://localhost:1935/live/13">
</div>
<div class="form-group">
<label for="taskName"><i class="fas fa-tag"></i> 任务名称</label>
<input type="text" id="taskName" name="taskname" required
placeholder="例如: 道路监控1" value="监控任务">
</div>
<div class="form-group">
<label for="modelName"><i class="fas fa-robot"></i> 模型选择</label>
<select id="modelName" name="model_name">
<option value="yolov8n.pt">YOLOv8 Nano (最快)</option>
<option value="yolov8s.pt">YOLOv8 Small</option>
<option value="yolov8m.pt" selected>YOLOv8 Medium</option>
<option value="yolov8l.pt">YOLOv8 Large</option>
<option value="yolov8x.pt">YOLOv8 XLarge (最准)</option>
</select>
</div>
<div class="form-group">
<label for="imgsz"><i class="fas fa-expand-arrows-alt"></i> 推理尺寸</label>
<select id="imgsz" name="imgsz">
<option value="320">320px (最快)</option>
<option value="640" selected>640px (推荐)</option>
<option value="1280">1280px (高清)</option>
</select>
</div>
<div class="form-group">
<label for="device"><i class="fas fa-microchip"></i> 推理设备</label>
<select id="device" name="device">
<option value="auto">自动选择</option>
<option value="cuda:0">GPU (CUDA)</option>
<option value="cpu">CPU</option>
</select>
</div>
<div class="form-group">
<label for="confidence"><i class="fas fa-chart-line"></i> 置信度阈值</label>
<input type="range" id="confidence" name="conf_thres" min="0.1" max="0.9" step="0.05" value="0.25">
<div style="display: flex; justify-content: space-between; margin-top: 5px;">
<span>0.1</span>
<span id="confidenceValue">0.25</span>
<span>0.9</span>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" id="resetFormBtn">
<i class="fas fa-redo"></i> 重置表单
</button>
<button type="submit" class="btn btn-primary" id="createTaskBtn">
<i class="fas fa-rocket"></i> 创建并启动任务
</button>
</div>
</form>
</div>
</div>
<!-- 任务列表 -->
<div class="tasks-container">
<div class="tasks-header">
<h2><i class="fas fa-list"></i> 任务列表</h2>
<div class="tasks-count">
任务: <span id="tasksCountBadge">0</span> | 活动: <span id="activeTasksBadge">0</span>
</div>
</div>
<div class="tabs">
<button class="tab active" data-tab="all">全部任务</button>
<button class="tab" data-tab="running">运行中</button>
<button class="tab" data-tab="stopped">已停止</button>
</div>
<div class="tab-content active" id="tab-all">
<div class="tasks-grid" id="tasksGrid">
<!-- 任务卡片将动态生成 -->
</div>
</div>
<div class="tab-content" id="tab-running">
<div class="tasks-grid" id="runningTasksGrid">
<!-- 运行中的任务卡片将动态生成 -->
</div>
</div>
<div class="tab-content" id="tab-stopped">
<div class="tasks-grid" id="stoppedTasksGrid">
<!-- 已停止的任务卡片将动态生成 -->
</div>
</div>
<div class="empty-state" id="emptyTasksState" style="display: none;">
<i class="fas fa-tasks"></i>
<h3>暂无任务</h3>
<p>点击上方"创建新检测任务"按钮开始您的第一个任务</p>
</div>
</div>
<!-- 实时检测面板 -->
<div class="realtime-panel" id="realtimePanel" style="display: none;">
<div class="form-header">
<h2><i class="fas fa-eye"></i> 实时检测结果</h2>
<button class="form-toggle-btn" id="toggleRealtimeBtn">
<i class="fas fa-chevron-down"></i> 展开面板
</button>
</div>
<div class="collapse-content" id="realtimeContent">
<div id="realtimeDetections">
<!-- 实时检测结果将在这里显示 -->
</div>
<div class="empty-state" id="emptyDetectionsState">
<i class="fas fa-search"></i>
<h3>暂无检测结果</h3>
<p>等待任务开始检测...</p>
</div>
</div>
</div>
</div>
<!-- 任务详情模态框 -->
<div class="modal" id="taskDetailModal">
<div class="modal-content">
<div class="modal-header">
<h3>任务详情</h3>
<button class="modal-close" id="closeDetailModal">&times;</button>
</div>
<div class="modal-body">
<div id="taskDetailContent">
<!-- 任务详情将动态加载 -->
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="closeDetailBtn">关闭</button>
</div>
</div>
</div>
<!-- 确认模态框 -->
<div class="modal" id="confirmModal">
<div class="modal-content">
<div class="modal-header">
<h3 id="confirmTitle">确认操作</h3>
<button class="modal-close" id="closeConfirmModal">&times;</button>
</div>
<div class="modal-body">
<p id="confirmMessage">您确定要执行此操作吗?</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancelConfirmBtn">取消</button>
<button class="btn btn-danger" id="confirmActionBtn">确认</button>
</div>
</div>
</div>
<!-- 通知容器 -->
<div id="notificationContainer"></div>
<script>
// 全局变量
let socket = null;
let currentTasks = {};
let activeTasks = {};
let stoppedTasks = {};
let currentTaskId = null;
let confirmationCallback = null;
let systemStatusInterval = null;
let tasksRefreshInterval = null;
// DOM加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initWebSocket();
initEventListeners();
loadTasks();
loadSystemStatus();
startAutoRefresh();
});
// 初始化WebSocket连接
function initWebSocket() {
socket = io();
socket.on('connect', function() {
updateConnectionStatus(true);
showNotification('WebSocket连接成功', 'success');
console.log('WebSocket连接成功');
});
socket.on('disconnect', function() {
updateConnectionStatus(false);
showNotification('WebSocket连接断开', 'warning');
console.log('WebSocket连接断开');
});
socket.on('connect_error', function(error) {
updateConnectionStatus(false);
showNotification('WebSocket连接错误: ' + error.message, 'error');
console.error('WebSocket连接错误:', error);
});
socket.on('detection_results', function(data) {
handleDetectionResults(data);
});
}
// 更新连接状态显示
function updateConnectionStatus(connected) {
const statusDot = document.getElementById('connectionStatus');
const statusText = document.getElementById('connectionText');
if (connected) {
statusDot.style.backgroundColor = '#27ae60';
statusText.textContent = '已连接';
} else {
statusDot.style.backgroundColor = '#e74c3c';
statusText.textContent = '未连接';
}
}
// 初始化事件监听器
function initEventListeners() {
// 表单折叠/展开
const toggleFormBtn = document.getElementById('toggleFormBtn');
const formContent = document.getElementById('taskFormContent');
toggleFormBtn.addEventListener('click', function() {
const isExpanded = formContent.classList.contains('show');
if (isExpanded) {
formContent.classList.remove('show');
toggleFormBtn.innerHTML = '<i class="fas fa-chevron-down"></i> 展开表单';
} else {
formContent.classList.add('show');
toggleFormBtn.innerHTML = '<i class="fas fa-chevron-up"></i> 收起表单';
}
});
// 实时面板折叠/展开
const toggleRealtimeBtn = document.getElementById('toggleRealtimeBtn');
const realtimeContent = document.getElementById('realtimeContent');
toggleRealtimeBtn.addEventListener('click', function() {
const isExpanded = realtimeContent.classList.contains('show');
if (isExpanded) {
realtimeContent.classList.remove('show');
toggleRealtimeBtn.innerHTML = '<i class="fas fa-chevron-down"></i> 展开面板';
} else {
realtimeContent.classList.add('show');
toggleRealtimeBtn.innerHTML = '<i class="fas fa-chevron-up"></i> 收起面板';
}
});
// 置信度滑块
const confidenceSlider = document.getElementById('confidence');
const confidenceValue = document.getElementById('confidenceValue');
confidenceSlider.addEventListener('input', function() {
confidenceValue.textContent = this.value;
});
// 表单提交
const createTaskForm = document.getElementById('createTaskForm');
createTaskForm.addEventListener('submit', function(e) {
e.preventDefault();
createTask();
});
// 重置表单
const resetFormBtn = document.getElementById('resetFormBtn');
resetFormBtn.addEventListener('click', function() {
createTaskForm.reset();
confidenceValue.textContent = '0.25';
showNotification('表单已重置', 'info');
});
// 选项卡切换
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.addEventListener('click', function() {
const tabId = this.getAttribute('data-tab');
// 移除所有active类
tabs.forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// 添加active类
this.classList.add('active');
document.getElementById(`tab-${tabId}`).classList.add('active');
});
});
// 模态框关闭
document.getElementById('closeDetailModal').addEventListener('click', closeTaskDetailModal);
document.getElementById('closeDetailBtn').addEventListener('click', closeTaskDetailModal);
document.getElementById('closeConfirmModal').addEventListener('click', closeConfirmModal);
document.getElementById('cancelConfirmBtn').addEventListener('click', closeConfirmModal);
// 点击模态框外部关闭
window.addEventListener('click', function(e) {
if (e.target.id === 'taskDetailModal') {
closeTaskDetailModal();
}
if (e.target.id === 'confirmModal') {
closeConfirmModal();
}
});
// 键盘事件
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeTaskDetailModal();
closeConfirmModal();
}
});
}
// 创建新任务
async function createTask() {
const form = document.getElementById('createTaskForm');
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// 处理设备选择
if (data.device === 'auto') {
delete data.device; // 使用后端默认
}
// 添加置信度阈值
data.conf_thres = document.getElementById('confidence').value;
// 禁用提交按钮并显示加载状态
const submitBtn = document.getElementById('createTaskBtn');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<div class="loader"></div> 创建中...';
submitBtn.disabled = true;
try {
const response = await fetch('/api/tasks/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.status === 'success') {
showNotification(`任务创建成功任务ID: ${result.task_id}`, 'success');
// 清空表单(可选)
// form.reset();
// document.getElementById('confidenceValue').textContent = '0.25';
// 刷新任务列表
loadTasks();
} else {
showNotification(`创建失败: ${result.message}`, 'error');
}
} catch (error) {
showNotification(`请求失败: ${error.message}`, 'error');
console.error('创建任务失败:', error);
} finally {
// 恢复按钮状态
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
}
// 加载所有任务
async function loadTasks() {
try {
const response = await fetch('/api/tasks');
const result = await response.json();
if (result.status === 'success') {
currentTasks = {};
activeTasks = {};
stoppedTasks = {};
// 更新任务计数
document.getElementById('tasksCount').textContent =
`${result.data.active}/${result.data.total}`;
document.getElementById('tasksCountBadge').textContent = result.data.total;
document.getElementById('activeTasksBadge').textContent = result.data.active;
document.getElementById('activeTasks').textContent = result.data.active;
document.getElementById('totalTasks').textContent = result.data.total;
// 清空任务网格
document.getElementById('tasksGrid').innerHTML = '';
document.getElementById('runningTasksGrid').innerHTML = '';
document.getElementById('stoppedTasksGrid').innerHTML = '';
// 显示/隐藏空状态
const emptyState = document.getElementById('emptyTasksState');
if (result.data.tasks.length === 0) {
emptyState.style.display = 'block';
} else {
emptyState.style.display = 'none';
}
// 处理每个任务
result.data.tasks.forEach(task => {
currentTasks[task.task_id] = task;
if (task.status === 'running' || task.status === 'starting') {
activeTasks[task.task_id] = task;
} else {
stoppedTasks[task.task_id] = task;
}
// 添加到所有任务网格
document.getElementById('tasksGrid').appendChild(createTaskCard(task));
// 添加到相应状态网格
if (task.status === 'running' || task.status === 'starting') {
document.getElementById('runningTasksGrid').appendChild(createTaskCard(task));
} else {
document.getElementById('stoppedTasksGrid').appendChild(createTaskCard(task));
}
});
// 显示/隐藏实时检测面板
const realtimePanel = document.getElementById('realtimePanel');
if (Object.keys(activeTasks).length > 0) {
realtimePanel.style.display = 'block';
} else {
realtimePanel.style.display = 'none';
}
}
} catch (error) {
console.error('加载任务失败:', error);
showNotification('加载任务列表失败', 'error');
}
}
// 创建任务卡片
function createTaskCard(task) {
const card = document.createElement('div');
card.className = `task-card ${task.status}`;
card.id = `task-${task.task_id}`;
// 状态图标
let statusIcon, statusText, statusClass;
switch(task.status) {
case 'running':
statusIcon = 'fa-play-circle';
statusText = '运行中';
statusClass = 'status-running';
break;
case 'starting':
statusIcon = 'fa-spinner fa-spin';
statusText = '启动中';
statusClass = 'status-starting';
break;
case 'stopped':
statusIcon = 'fa-stop-circle';
statusText = '已停止';
statusClass = 'status-stopped';
break;
case 'creating':
statusIcon = 'fa-plus-circle';
statusText = '创建中';
statusClass = 'status-creating';
break;
default:
statusIcon = 'fa-question-circle';
statusText = task.status;
statusClass = 'status-stopped';
}
// 格式化时间
const createdTime = new Date(task.created_at).toLocaleString('zh-CN');
card.innerHTML = `
<div class="task-header">
<div class="task-title">
<i class="fas fa-video task-icon"></i>
<span class="task-name">${task.config.taskname}</span>
</div>
<div class="task-status ${statusClass}">
<i class="fas ${statusIcon}"></i> ${statusText}
</div>
</div>
<div class="task-body">
<div class="task-info">
<div class="info-row">
<span class="info-label">任务ID:</span>
<span class="info-value" title="${task.task_id}">${task.task_id.substring(0, 8)}...</span>
</div>
<div class="info-row">
<span class="info-label">创建时间:</span>
<span class="info-value">${createdTime}</span>
</div>
<div class="info-row">
<span class="info-label">模型:</span>
<span class="info-value">${task.config.model}</span>
</div>
<div class="info-row">
<span class="info-label">RTMP地址:</span>
<span class="info-value" title="${task.config.rtmp_url}">${truncateText(task.config.rtmp_url, 25)}</span>
</div>
</div>
<div class="task-stats">
<div class="stats-row">
<div class="stat">
<div class="stat-value">${task.stats.total_frames || 0}</div>
<div class="stat-label">总帧数</div>
</div>
<div class="stat">
<div class="stat-value">${task.stats.detections || 0}</div>
<div class="stat-label">检测数</div>
</div>
<div class="stat">
<div class="stat-value">${task.stats.avg_fps || 0}</div>
<div class="stat-label">FPS</div>
</div>
</div>
</div>
<div class="task-actions">
${task.status === 'running' ?
`<button class="btn btn-warning" onclick="stopTask('${task.task_id}')">
<i class="fas fa-stop"></i> 停止
</button>` :
task.status === 'stopped' ?
`<button class="btn btn-success" onclick="startTask('${task.task_id}')">
<i class="fas fa-play"></i> 启动
</button>` :
`<button class="btn btn-secondary" disabled>
<i class="fas fa-spinner fa-spin"></i> ${statusText}
</button>`
}
<button class="btn btn-secondary" onclick="viewTaskDetail('${task.task_id}')">
<i class="fas fa-info-circle"></i> 详情
</button>
<button class="btn btn-danger" onclick="cleanupTask('${task.task_id}')">
<i class="fas fa-trash"></i> 清理
</button>
</div>
</div>
`;
return card;
}
// 停止任务
async function stopTask(taskId) {
showConfirmation('停止任务', '确定要停止这个任务吗?任务将停止检测视频流。', async () => {
try {
const response = await fetch(`/api/tasks/${taskId}/stop`, {
method: 'POST'
});
const result = await response.json();
if (result.status === 'success') {
showNotification(`任务已停止: ${taskId.substring(0, 8)}...`, 'success');
loadTasks();
} else {
showNotification(`停止失败: ${result.message}`, 'error');
}
} catch (error) {
showNotification(`停止失败: ${error.message}`, 'error');
console.error('停止任务失败:', error);
}
});
}
// 启动任务
async function startTask(taskId) {
// 由于API设计重新启动任务需要重新创建
showNotification('请使用创建新任务功能启动新任务', 'info');
}
// 清理任务
async function cleanupTask(taskId) {
showConfirmation('清理任务', '确定要清理这个任务吗?这将释放所有资源,操作不可撤销。', async () => {
try {
const response = await fetch(`/api/tasks/${taskId}/cleanup`, {
method: 'POST'
});
const result = await response.json();
if (result.status === 'success') {
showNotification(`任务已清理: ${taskId.substring(0, 8)}...`, 'success');
loadTasks();
} else {
showNotification(`清理失败: ${result.message}`, 'error');
}
} catch (error) {
showNotification(`清理失败: ${error.message}`, 'error');
console.error('清理任务失败:', error);
}
});
}
// 查看任务详情
async function viewTaskDetail(taskId) {
try {
const response = await fetch(`/api/tasks/${taskId}/status`);
const result = await response.json();
if (result.status === 'success') {
const task = result.data;
const detailContent = document.getElementById('taskDetailContent');
// 格式化配置信息
const configHtml = Object.entries(task.config).map(([key, value]) => {
if (typeof value === 'object') {
const nestedHtml = Object.entries(value).map(([nestedKey, nestedValue]) => {
return `<div class="info-row">
<span class="info-label">${nestedKey}:</span>
<span class="info-value">${JSON.stringify(nestedValue)}</span>
</div>`;
}).join('');
return `<h4>${key}</h4>${nestedHtml}`;
} else {
return `<div class="info-row">
<span class="info-label">${key}:</span>
<span class="info-value">${value}</span>
</div>`;
}
}).join('');
detailContent.innerHTML = `
<div class="task-info">
<div class="info-row">
<span class="info-label">任务ID:</span>
<span class="info-value">${task.task_id}</span>
</div>
<div class="info-row">
<span class="info-label">状态:</span>
<span class="info-value">
<span class="task-status ${task.status === 'running' ? 'status-running' : 'status-stopped'}">
${task.status}
</span>
</span>
</div>
<div class="info-row">
<span class="info-label">创建时间:</span>
<span class="info-value">${new Date(task.created_at).toLocaleString('zh-CN')}</span>
</div>
<h4 style="margin-top: 20px; margin-bottom: 10px;">任务配置</h4>
${configHtml}
<h4 style="margin-top: 20px; margin-bottom: 10px;">统计信息</h4>
<div class="task-stats">
<div class="stats-row">
<div class="stat">
<div class="stat-value">${task.stats.total_frames || 0}</div>
<div class="stat-label">总帧数</div>
</div>
<div class="stat">
<div class="stat-value">${task.stats.detections || 0}</div>
<div class="stat-label">检测数</div>
</div>
<div class="stat">
<div class="stat-value">${task.stats.avg_fps || 0}</div>
<div class="stat-label">FPS</div>
</div>
</div>
</div>
</div>
`;
// 显示模态框
document.getElementById('taskDetailModal').style.display = 'flex';
}
} catch (error) {
showNotification(`获取任务详情失败: ${error.message}`, 'error');
console.error('获取任务详情失败:', error);
}
}
// 关闭任务详情模态框
function closeTaskDetailModal() {
document.getElementById('taskDetailModal').style.display = 'none';
}
// 显示确认对话框
function showConfirmation(title, message, callback) {
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
confirmationCallback = callback;
document.getElementById('confirmModal').style.display = 'flex';
// 设置确认按钮点击事件
document.getElementById('confirmActionBtn').onclick = function() {
if (confirmationCallback) {
confirmationCallback();
}
closeConfirmModal();
};
}
// 关闭确认模态框
function closeConfirmModal() {
document.getElementById('confirmModal').style.display = 'none';
confirmationCallback = null;
}
// 加载系统状态
async function loadSystemStatus() {
try {
const response = await fetch('/api/system/status');
const result = await response.json();
if (result.status === 'success') {
const data = result.data;
// 更新CPU信息
document.getElementById('cpuPercent').textContent = `${data.cpu_percent.toFixed(1)}%`;
document.getElementById('cpuProgress').style.width = `${data.cpu_percent}%`;
// 更新内存信息
document.getElementById('memoryPercent').textContent = `${data.memory_percent.toFixed(1)}%`;
document.getElementById('memoryProgress').style.width = `${data.memory_percent}%`;
// 更新任务信息
document.getElementById('tasksCount').textContent = `${data.active_tasks}/${data.total_tasks}`;
// 更新GPU信息
const gpuStatus = document.getElementById('gpuStatus');
const gpuInfo = document.getElementById('gpuInfo');
if (data.gpus && data.gpus.length > 0) {
const gpu = data.gpus[0];
gpuStatus.textContent = `${gpu.load.toFixed(1)}%`;
gpuInfo.textContent = `${gpu.name} (${gpu.memory_used.toFixed(0)}/${gpu.memory_total.toFixed(0)} MB)`;
} else {
gpuStatus.textContent = '不可用';
gpuInfo.textContent = '无GPU或未安装监控';
}
}
} catch (error) {
console.error('加载系统状态失败:', error);
}
}
// 处理检测结果
function handleDetectionResults(data) {
// 显示实时检测面板(如果隐藏)
const realtimePanel = document.getElementById('realtimePanel');
if (realtimePanel.style.display === 'none') {
realtimePanel.style.display = 'block';
}
// 隐藏空状态
document.getElementById('emptyDetectionsState').style.display = 'none';
// 获取检测结果容器
const detectionsContainer = document.getElementById('realtimeDetections');
// 清空之前的检测结果(可选,或者限制显示数量)
// detectionsContainer.innerHTML = '';
// 创建检测结果卡片
if (data.detections && data.detections.length > 0) {
// 只显示最新的5个检测结果
while (detectionsContainer.children.length >= 10) {
detectionsContainer.removeChild(detectionsContainer.firstChild);
}
data.detections.forEach(detection => {
const detectionCard = document.createElement('div');
detectionCard.className = 'detection-card';
detectionCard.innerHTML = `
<div class="detection-header">
<span class="detection-class">${detection.class_name}</span>
<span class="detection-confidence">${(detection.confidence * 100).toFixed(1)}%</span>
</div>
<div class="detection-box">
位置: [${detection.box.map(c => c.toFixed(1)).join(', ')}]<br>
任务: ${data.taskname || data.task_id.substring(0, 8)} | 时间: ${data.time_str}
</div>
`;
// 添加到顶部(最新结果显示在最上面)
detectionsContainer.insertBefore(detectionCard, detectionsContainer.firstChild);
});
}
// 更新对应任务卡片的实时数据
if (currentTasks[data.task_id]) {
const taskCard = document.getElementById(`task-${data.task_id}`);
if (taskCard) {
// 更新FPS和检测数
const statsRow = taskCard.querySelector('.stats-row');
if (statsRow) {
const fpsElement = statsRow.querySelectorAll('.stat-value')[2];
if (fpsElement) {
fpsElement.textContent = data.fps.toFixed(1);
}
// 更新检测数(累加)
const detectionsElement = statsRow.querySelectorAll('.stat-value')[1];
if (detectionsElement) {
const currentDetections = parseInt(detectionsElement.textContent) || 0;
detectionsElement.textContent = currentDetections + data.detections.length;
}
// 更新帧数
const framesElement = statsRow.querySelectorAll('.stat-value')[0];
if (framesElement) {
framesElement.textContent = data.frame_count;
}
}
}
}
}
// 显示通知
function showNotification(message, type = 'info') {
const container = document.getElementById('notificationContainer');
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
container.appendChild(notification);
// 3秒后移除通知
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 3000);
}
// 启动自动刷新
function startAutoRefresh() {
// 每5秒刷新一次系统状态
systemStatusInterval = setInterval(loadSystemStatus, 5000);
// 每10秒刷新一次任务列表
tasksRefreshInterval = setInterval(loadTasks, 10000);
}
// 停止自动刷新
function stopAutoRefresh() {
if (systemStatusInterval) clearInterval(systemStatusInterval);
if (tasksRefreshInterval) clearInterval(tasksRefreshInterval);
}
// 工具函数:截断文本
function truncateText(text, maxLength) {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
}
// 页面卸载时清理
window.addEventListener('beforeunload', function() {
stopAutoRefresh();
if (socket) socket.disconnect();
});
</script>
</body>
</html>