Yolov/templates/task_management.html

1749 lines
60 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!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>