1749 lines
60 KiB
HTML
1749 lines
60 KiB
HTML
<!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">×</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">×</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> |