1273 lines
43 KiB
HTML
1273 lines
43 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>Device</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
|
|
<style>
|
|
/* 基础样式优化 */
|
|
body {
|
|
background-color: #f8f9fa;
|
|
color: #212529;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* 卡片样式现代化 */
|
|
.card {
|
|
border: none;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
margin-bottom: 24px;
|
|
}
|
|
.card-title {
|
|
font-size: 1.25rem; /* 20px */
|
|
font-weight: 600; /* 稍微加粗 */
|
|
margin-bottom: 0.75rem;
|
|
color: #2c3e50; /* 深色系,提高对比度 */
|
|
}
|
|
|
|
.card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
|
}
|
|
|
|
.card-body {
|
|
padding: 24px;
|
|
}
|
|
|
|
/* 按钮样式优化 */
|
|
.btn {
|
|
border-radius: 8px; /* 减小圆角 */
|
|
padding: 6px 10px; /* 减小内边距 */
|
|
font-weight: 500;
|
|
font-size: 0.8rem; /* 14px */
|
|
transition: all 0.2s;
|
|
line-height: 1; /* 优化行高 */
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
backdrop-filter: blur(8px);
|
|
transition: all 0.3s ease;
|
|
}
|
|
.btn-sm { /* 更小尺寸的按钮 */
|
|
padding: 4px 10px;
|
|
font-size: 0.8rem; /* 13px */
|
|
border-radius: 8px;
|
|
line-height: 1;
|
|
}
|
|
.btn-warning {
|
|
background: linear-gradient(45deg, #6897fe, #3979e0); /* 高级深蓝色 */
|
|
border: none;
|
|
color: white;
|
|
}
|
|
|
|
.btn-warning:hover {
|
|
background: linear-gradient(45deg, #3979e0, #6897fe);
|
|
transform: translateY(-1px);
|
|
color: white;
|
|
}
|
|
|
|
.btn-success {
|
|
background: linear-gradient(45deg, #1fbff4, #61caf7); /* 明亮蓝色 */
|
|
border: none;
|
|
color: white;
|
|
}
|
|
|
|
.btn-success:hover {
|
|
background: linear-gradient(45deg, #61caf7, #1fbff4);
|
|
transform: translateY(-1px);
|
|
color: white;
|
|
}
|
|
|
|
.btn-info {
|
|
background: linear-gradient(45deg, #5ddcbf, #76ede0); /* 薄荷绿色 */
|
|
border: none;
|
|
color: white;
|
|
}
|
|
|
|
.btn-info:hover {
|
|
background: linear-gradient(45deg,#76ede0, #5ddcbf);
|
|
transform: translateY(-1px);
|
|
color: white;
|
|
}
|
|
|
|
|
|
.btn-danger {
|
|
background-color: #d3d3d3; /* 使用柔和的灰蓝色替代红色 */
|
|
border: none;
|
|
color: white;
|
|
min-width: 40px;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background-color: #b8b8b8; /* 悬停时加深色调 */
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: linear-gradient(45deg, #a5a4a4, #7b8389);
|
|
border: none;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: linear-gradient(45deg, #7b8389, #a5a4a4);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
/* 导航栏样式 */
|
|
.navbar {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 64px;
|
|
background: var(--surface-color, #FFFFFF);
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 24px;
|
|
z-index: 1000;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.navbar-brand {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: var(--primary-color, #6366F1);
|
|
margin-right: 48px;
|
|
}
|
|
|
|
.navbar-menu {
|
|
display: flex;
|
|
gap: 32px;
|
|
}
|
|
|
|
/* 修改导航栏菜单项样式 */
|
|
.navbar-menu-item {
|
|
padding: 8px 16px;
|
|
cursor: pointer;
|
|
border-radius: 8px;
|
|
transition: all 0.3s;
|
|
color: var(--text-secondary, #64748B);
|
|
font-weight: 500;
|
|
background: transparent; /* 默认透明背景 */
|
|
}
|
|
|
|
/* 修改悬停和激活状态样式 */
|
|
.navbar-menu-item:hover {
|
|
background: linear-gradient(145deg, var(--primary-color, #6366F1), var(--primary-light, #818CF8));
|
|
color: white;
|
|
}
|
|
|
|
.navbar-menu-item.active {
|
|
background: linear-gradient(145deg, var(--primary-color, #6366F1), var(--primary-light, #818CF8));
|
|
color: white;
|
|
}
|
|
|
|
.navbar-right {
|
|
margin-left: auto;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 24px;
|
|
color: var(--text-secondary, #64748B);
|
|
}
|
|
|
|
/* 更新按钮样式 */
|
|
.navbar .btn {
|
|
padding: 8px 16px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-weight: 500;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.navbar .btn-primary {
|
|
background: linear-gradient(145deg, var(--primary-color, #6366F1), var(--primary-light, #818CF8));
|
|
color: white;
|
|
border: none;
|
|
}
|
|
|
|
.navbar .btn-secondary {
|
|
background: var(--background-color, #F8FAFC);
|
|
color: var(--text-secondary, #64748B);
|
|
border: none;
|
|
}
|
|
|
|
.navbar .btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
|
|
}
|
|
|
|
/* 调整主内容区域的上边距,以适应固定导航栏 */
|
|
#mainContent {
|
|
margin-top: 64px;
|
|
}
|
|
|
|
/* 图表容器优化 */
|
|
.charts-container {
|
|
display: flex;
|
|
flex-direction: column; /* 垂直排列 */
|
|
gap: 20px; /* 图表之间的间距 */
|
|
width: 100%;
|
|
padding: 20px;
|
|
}
|
|
|
|
.chart-wrapper {
|
|
width: 100%;
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.chart-container {
|
|
height: 300px; /* 每个图表的固定高度 */
|
|
width: 100%;
|
|
}
|
|
|
|
.chart-wrapper h5 {
|
|
margin: 0 0 10px 0;
|
|
color: #333;
|
|
font-size: 16px;
|
|
}
|
|
|
|
/* 数据显示区域优化 */
|
|
.data-container {
|
|
background-color: white;
|
|
padding: 20px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
|
}
|
|
|
|
/* 表单控件美化 */
|
|
.form-control {
|
|
border-radius: 8px;
|
|
border: 1px solid #dee2e6;
|
|
padding: 10px 16px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.form-control:focus {
|
|
border-color: #4a6bff;
|
|
box-shadow: 0 0 0 3px rgba(74,107,255,0.1);
|
|
}
|
|
|
|
/* 模态框优化 */
|
|
.modal-content {
|
|
border: none;
|
|
border-radius: 16px;
|
|
}
|
|
|
|
.modal-header {
|
|
border-bottom: 1px solid #eee;
|
|
padding: 20px 24px;
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 24px;
|
|
}
|
|
|
|
/* 实验数据显示区域 */
|
|
#realTimeData {
|
|
height: 300px;
|
|
border-radius: 12px;
|
|
background: white;
|
|
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
|
}
|
|
|
|
/* 加载动画优化 */
|
|
.spinner-border {
|
|
width: 1.5rem;
|
|
height: 1.5rem;
|
|
border-width: 0.2em;
|
|
}
|
|
|
|
/* 警告提示框美化 */
|
|
.alert {
|
|
border: none;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
|
}
|
|
|
|
/* 现代化表格容器样式 */
|
|
.table-responsive {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
margin-bottom: 16px;
|
|
max-height: 250px;
|
|
border-radius: 12px;
|
|
background: #ffffff;
|
|
padding: 8px;
|
|
}
|
|
|
|
/* 现代化表格样式 */
|
|
.table-sm {
|
|
width: 100%;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
}
|
|
|
|
.table-sm th {
|
|
padding: 12px 16px;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
color: #475569;
|
|
background: #fafafa;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
border-bottom: 2px solid #fafafa;
|
|
}
|
|
|
|
.table-sm td {
|
|
padding: 12px 16px;
|
|
font-size: 0.875rem;
|
|
color: #334155;
|
|
border-bottom: 1px solid #ffffff;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.table-sm tr:hover td {
|
|
background-color: #ffffff;
|
|
}
|
|
|
|
/* 设备操作按钮容器样式 */
|
|
.device-actions {
|
|
margin-top: auto;
|
|
padding-top: 16px;
|
|
border-top: 1px solid #ffffff;
|
|
}
|
|
|
|
/* 列布局样式 */
|
|
.col-md-4 {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
/* 自定义滚动条样式 */
|
|
.table-responsive::-webkit-scrollbar {
|
|
width: 6px;
|
|
height: 6px;
|
|
}
|
|
|
|
.table-responsive::-webkit-scrollbar-track {
|
|
background: #ffffff;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.table-responsive::-webkit-scrollbar-thumb {
|
|
background: #e8ebf0;
|
|
border-radius: 8px;
|
|
transition: background 0.2s ease;
|
|
}
|
|
|
|
.table-responsive::-webkit-scrollbar-thumb:hover {
|
|
background: #e8ebf0;
|
|
}
|
|
|
|
/* 表格空状态样式 */
|
|
.table-sm tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
/* 添加表格斑马纹 */
|
|
.table-sm tr:nth-child(even) {
|
|
background-color: #fafafa;
|
|
}
|
|
|
|
/* 添加表格单元格动画效果 */
|
|
.table-sm td {
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.table-sm td::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
bottom: 0;
|
|
width: 100%;
|
|
height: 1px;
|
|
background: #3b82f6;
|
|
transform: scaleX(0);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.table-sm tr:hover td::after {
|
|
transform: scaleX(1);
|
|
}
|
|
.hidden {
|
|
display: none;
|
|
}
|
|
.page-content {
|
|
display: none;
|
|
}
|
|
.page-content.active {
|
|
display: block;
|
|
}
|
|
.charts-container {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 20px;
|
|
margin: 20px 0;
|
|
}
|
|
.chart-wrapper {
|
|
background: white;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
width: calc(50% - 10px);
|
|
min-width: 300px;
|
|
}
|
|
.chart-container {
|
|
position: relative;
|
|
height: 250px;
|
|
width: 100%;
|
|
}
|
|
|
|
/* 按钮禁用状态样式 */
|
|
.btn:disabled,
|
|
.btn[disabled] {
|
|
background-color: #b8b8b8 !important; /* 使用 !important 确保覆盖其他样式 */
|
|
color: white !important;
|
|
border: none;
|
|
opacity: 0.65 !important; /* 防止 Bootstrap 默认的透明度 */
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* 确保禁用状态下的渐变背景也变为灰色 */
|
|
.btn-warning:disabled,
|
|
.btn-primary:disabled,
|
|
.btn-info:disabled {
|
|
background: #b8b8b8 !important;
|
|
background-image: none !important;
|
|
}
|
|
|
|
/* 禁用状态下取消悬停效果 */
|
|
.btn:disabled:hover {
|
|
transform: none !important;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
|
|
}
|
|
/* 禁用状态下的按钮样式 */
|
|
.btn-success:disabled,
|
|
.btn-success[disabled] {
|
|
background: #adb0b4 !important; /* 使用纯色背景 */
|
|
background-image: none !important; /* 移除渐变效果 */
|
|
background-color: #adb0b4 !important;
|
|
border-color: #adb0b4 !important;
|
|
color: #fff !important;
|
|
opacity: 0.65 !important;
|
|
pointer-events: none;
|
|
transform: none !important;
|
|
}
|
|
|
|
/* 确保禁用状态下不会显示悬停效果 */
|
|
.btn-success:disabled:hover,
|
|
.btn-success[disabled]:hover {
|
|
background: #adb0b4 !important;
|
|
background-image: none !important;
|
|
background-color: #adb0b4 !important;
|
|
border-color: #adb0b4 !important;
|
|
transform: none !important;
|
|
}
|
|
/* 侧边栏样式 */
|
|
.sidebar {
|
|
position: fixed;
|
|
top: var(--header-height);
|
|
left: 0;
|
|
bottom: 0;
|
|
width: var(--sidebar-width);
|
|
background: var(--surface-color);
|
|
border-right: 1px solid var(--border-color);
|
|
}
|
|
|
|
.sidebar-menu {
|
|
list-style: none;
|
|
padding: 16px 0;
|
|
}
|
|
|
|
.sidebar-item {
|
|
padding: 12px 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
margin: 4px 8px;
|
|
border-radius: 8px;
|
|
color: var(--text-secondary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.sidebar-item:hover {
|
|
background: linear-gradient(145deg, var(--primary-color), var(--primary-light));
|
|
color: white;
|
|
}
|
|
|
|
.sidebar-item.active {
|
|
background: linear-gradient(145deg, var(--primary-color), var(--primary-light));
|
|
color: white;
|
|
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
|
|
}
|
|
|
|
.sidebar-item .bi {
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
/* 添加CSS变量 */
|
|
:root {
|
|
--header-height: 64px;
|
|
--sidebar-width: 240px;
|
|
--surface-color: #FFFFFF;
|
|
--border-color: #E2E8F0;
|
|
--primary-color: #6366F1;
|
|
--primary-light: #818CF8;
|
|
--text-secondary: #64748B;
|
|
}
|
|
|
|
/* 移除卡片样式 */
|
|
.sidebar.card {
|
|
border: none;
|
|
box-shadow: none;
|
|
background: none;
|
|
}
|
|
|
|
.sidebar .card-body {
|
|
padding: 0;
|
|
}
|
|
|
|
/* 主要内容区域 */
|
|
.main-content {
|
|
margin-left: var(--sidebar-width);
|
|
margin-top: var(--header-height);
|
|
padding: 24px 32px;
|
|
}
|
|
|
|
.page-header {
|
|
margin-bottom: 24px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
background: linear-gradient(45deg, var(--primary-color), var(--primary-light));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
/* 更新卡片样式 */
|
|
.card {
|
|
background: var(--surface-color);
|
|
border-radius: 16px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
|
transition: transform 0.3s;
|
|
border: none;
|
|
}
|
|
|
|
.card:hover {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.card-header {
|
|
padding: 20px 24px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
background: transparent;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.card-body {
|
|
padding: 24px;
|
|
}
|
|
|
|
/* 更新按钮样式 */
|
|
.btn {
|
|
padding: 10px 20px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
font-weight: 500;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(145deg, var(--primary-color), var(--primary-light));
|
|
color: white;
|
|
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.3);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--background-color);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: var(--border-color);
|
|
}
|
|
|
|
/* 更新表单控件样式 */
|
|
.form-control {
|
|
padding: 10px 16px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
transition: all 0.3s;
|
|
background: var(--background-color);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.form-control:focus {
|
|
border-color: var(--primary-color);
|
|
outline: none;
|
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
|
}
|
|
|
|
/* 告部分切换样式 */
|
|
.report-section {
|
|
display: none;
|
|
}
|
|
|
|
.report-section.active {
|
|
display: block;
|
|
}
|
|
|
|
#chartsContainer {
|
|
display: flex;
|
|
flex-direction: column; /* 垂直排列 */
|
|
gap: 20px; /* 图表之间的间距 */
|
|
width: 100%;
|
|
padding: 20px;
|
|
}
|
|
|
|
.chart-wrapper {
|
|
width: 100%;
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.chart-container {
|
|
height: 300px; /* 每个图表的固定高度 */
|
|
width: 100%;
|
|
}
|
|
|
|
.chart-wrapper h5 {
|
|
margin: 0 0 10px 0;
|
|
color: #333;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.chart-data-panel {
|
|
background: #f8f9fa;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
|
height: 100%;
|
|
min-height: 300px;
|
|
}
|
|
|
|
.custom-data-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
|
|
.custom-data-item {
|
|
background: white;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.custom-data-item:hover {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.custom-data-name {
|
|
font-size: 0.9rem;
|
|
color: #666;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.custom-data-value {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
color: #2c3e50;
|
|
display: flex;
|
|
align-items: baseline;
|
|
}
|
|
|
|
.custom-data-unit {
|
|
font-size: 0.9rem;
|
|
color: #666;
|
|
margin-left: 5px;
|
|
}
|
|
/* 优化在线状态图标的样式 */
|
|
.bi-circle-fill.text-success {
|
|
color: #6366F1 !important;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
/* 响应式布局优化 */
|
|
@media (max-width: 991.98px) {
|
|
.chart-data-panel {
|
|
margin-top: 20px;
|
|
min-height: auto;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="navbar">
|
|
<div class="navbar-brand">Lab</div>
|
|
<div class="navbar-menu">
|
|
<div class="navbar-menu-item" onclick="window.location.href='lab.html'">Project</div>
|
|
<div class="navbar-menu-item active">Device</div>
|
|
<div class="navbar-menu-item" onclick="window.location.href='reports.html'">Reports</div>
|
|
<div class="navbar-menu-item" onclick="window.location.href='paper.html'">Paper</div>
|
|
</div>
|
|
<div class="navbar-right">
|
|
<a href="https://beta.obscura.work/lab/device-register.html" target="_blank" class="btn btn-primary">
|
|
<i class="bi bi-plus-circle"></i>
|
|
Device Registration
|
|
</a>
|
|
<span class="nav-item" id="userInfo"></span>
|
|
<button class="btn btn-secondary" id="logoutBtn" onclick="logout()">
|
|
<i class="bi bi-box-arrow-right"></i>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="main-content">
|
|
<div class="row">
|
|
<!-- 左侧侧边栏 -->
|
|
<div class="col-auto">
|
|
<div class="sidebar">
|
|
<div class="sidebar-menu">
|
|
<div class="sidebar-item" onclick="showDeviceList()">
|
|
<i class="bi bi-list-ul"></i>
|
|
<span>Device List</span>
|
|
</div>
|
|
<div class="sidebar-item" onclick="showOnlineDevices()">
|
|
<i class="bi bi-circle-fill text-success"></i>
|
|
<span>Online devices</span>
|
|
</div>
|
|
<div class="sidebar-item" onclick="showDevicesFilter()">
|
|
<i class="bi bi-funnel"></i>
|
|
<span>Filter</span>
|
|
</div>
|
|
<div class="sidebar-item" onclick="showDevicesSort()">
|
|
<i class="bi bi-sort-down"></i>
|
|
<span>Sort</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- 右侧主要内容区 -->
|
|
<div class="col ps-0 pe-4">
|
|
<div class="card w-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h3 class="page-title">Devices</h3>
|
|
<button class="btn btn-primary" onclick="showCreateDeviceModal()">
|
|
<i class="bi bi-plus-circle"></i>
|
|
Add
|
|
</button>
|
|
</div>
|
|
<div id="devicesList" class="row">
|
|
<!-- 设备列表将通过 JavaScript 动态加载 -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Device Modal -->
|
|
<div class="modal fade" id="createDeviceModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Add Device</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="createDeviceForm">
|
|
<div class="mb-3">
|
|
<label class="form-label">Device Serial Number</label>
|
|
<input type="text" class="form-control" id="deviceId" required>
|
|
<div class="form-text">Please enter the device serial number</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="addDevice()">Add</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Devices Filter Modal -->
|
|
<div class="modal fade" id="devicesFilterModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Filter Devices</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="deviceFilterForm">
|
|
<div class="mb-3">
|
|
<label class="form-label">Device Name</label>
|
|
<input type="text" class="form-control" id="filterDeviceName">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Device Type</label>
|
|
<select class="form-select" id="filterDeviceType">
|
|
<option value="">All</option>
|
|
<option value="Arduino">Arduino</option>
|
|
<option value="RaspberryPi">RaspberryPi</option>
|
|
<option value="Custom">Custom</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Channel Count</label>
|
|
<div class="row">
|
|
<div class="col">
|
|
<input type="number" class="form-control" id="filterMinChannels" placeholder="Min">
|
|
</div>
|
|
<div class="col">
|
|
<input type="number" class="form-control" id="filterMaxChannels" placeholder="Max">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick="resetDeviceFilters()">Reset</button>
|
|
<button type="button" class="btn btn-primary" onclick="applyDeviceFilters()">Apply</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Devices Sort Modal -->
|
|
<div class="modal fade" id="devicesSortModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Sort Devices</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="deviceSortForm">
|
|
<div class="mb-3">
|
|
<label class="form-label">Sort By</label>
|
|
<select class="form-select" id="deviceSortField">
|
|
<option value="device_name">Device Name</option>
|
|
<option value="device_type">Device Type</option>
|
|
<option value="device_number">Channel Count</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Order</label>
|
|
<select class="form-select" id="deviceSortOrder">
|
|
<option value="asc">Ascending</option>
|
|
<option value="desc">Descending</option>
|
|
</select>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" onclick="applyDeviceSort()">Apply</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
// API base URL
|
|
const API_BASE_URL = 'https://dev.obscura.work/lab';
|
|
let token = localStorage.getItem('token');
|
|
let onlineDevicesOnly = false;
|
|
|
|
// 检查token是否存在
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
if (!token) {
|
|
alert('Please login first');
|
|
window.location.href = 'lab.html';
|
|
return;
|
|
}
|
|
// 加载设备列表
|
|
loadDevices();
|
|
});
|
|
|
|
// 添加设备过滤和排序的全局变量
|
|
let deviceFilters = {
|
|
name: '',
|
|
type: '',
|
|
minChannels: '',
|
|
maxChannels: ''
|
|
};
|
|
|
|
let deviceSort = {
|
|
field: 'device_name',
|
|
order: 'asc'
|
|
};
|
|
|
|
// 显示设备过滤模态框
|
|
function showDevicesFilter() {
|
|
// 设当前过滤值
|
|
document.getElementById('filterDeviceName').value = deviceFilters.name;
|
|
document.getElementById('filterDeviceType').value = deviceFilters.type;
|
|
document.getElementById('filterMinChannels').value = deviceFilters.minChannels;
|
|
document.getElementById('filterMaxChannels').value = deviceFilters.maxChannels;
|
|
|
|
// 显示模态框
|
|
new bootstrap.Modal(document.getElementById('devicesFilterModal')).show();
|
|
}
|
|
|
|
// 显示设备排序模态框
|
|
function showDevicesSort() {
|
|
// 设置当前排序值
|
|
document.getElementById('deviceSortField').value = deviceSort.field;
|
|
document.getElementById('deviceSortOrder').value = deviceSort.order;
|
|
|
|
// 显示模态框
|
|
new bootstrap.Modal(document.getElementById('devicesSortModal')).show();
|
|
}
|
|
|
|
// 重置设备过滤条件
|
|
function resetDeviceFilters() {
|
|
deviceFilters = {
|
|
name: '',
|
|
type: '',
|
|
minChannels: '',
|
|
maxChannels: ''
|
|
};
|
|
|
|
// 重置表单
|
|
document.getElementById('deviceFilterForm').reset();
|
|
|
|
// 重新加载设备列表
|
|
loadDevices();
|
|
|
|
// 关闭模态框
|
|
bootstrap.Modal.getInstance(document.getElementById('devicesFilterModal')).hide();
|
|
}
|
|
|
|
// 应用设备过滤条件
|
|
function applyDeviceFilters() {
|
|
deviceFilters = {
|
|
name: document.getElementById('filterDeviceName').value,
|
|
type: document.getElementById('filterDeviceType').value,
|
|
minChannels: document.getElementById('filterMinChannels').value,
|
|
maxChannels: document.getElementById('filterMaxChannels').value
|
|
};
|
|
|
|
// 重新加载设备列表
|
|
loadDevices();
|
|
|
|
// 关闭模态框
|
|
bootstrap.Modal.getInstance(document.getElementById('devicesFilterModal')).hide();
|
|
}
|
|
|
|
// 应用设备排序
|
|
function applyDeviceSort() {
|
|
deviceSort = {
|
|
field: document.getElementById('deviceSortField').value,
|
|
order: document.getElementById('deviceSortOrder').value
|
|
};
|
|
|
|
// 重新加载设备列表
|
|
loadDevices();
|
|
|
|
// 关闭模态框
|
|
bootstrap.Modal.getInstance(document.getElementById('devicesSortModal')).hide();
|
|
}
|
|
|
|
// 显示创建设备模态框
|
|
function showCreateDeviceModal() {
|
|
document.getElementById('deviceId').value = ''; // 清空输入框
|
|
new bootstrap.Modal(document.getElementById('createDeviceModal')).show();
|
|
}
|
|
|
|
// 显示在线设备
|
|
function showOnlineDevices() {
|
|
// 重置过滤条件
|
|
deviceFilters = {
|
|
name: '',
|
|
type: '',
|
|
minChannels: '',
|
|
maxChannels: ''
|
|
};
|
|
|
|
// 设置在线设备过滤为true
|
|
onlineDevicesOnly = true;
|
|
|
|
// 移除所有按钮的激活状态
|
|
document.querySelectorAll('.sidebar-item').forEach(item => {
|
|
item.classList.remove('active');
|
|
});
|
|
|
|
// 激活Online devices按钮
|
|
const onlineDevicesButton = document.querySelector('.sidebar-item[onclick="showOnlineDevices()"]');
|
|
onlineDevicesButton.classList.add('active');
|
|
|
|
// 重新加载设备列表
|
|
loadDevices();
|
|
}
|
|
|
|
// 修改loadDevices函数以支持在线设备过滤
|
|
async function loadDevices() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/userdevices`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
let devices = await response.json();
|
|
|
|
// 只显示 active 状态的设备
|
|
devices = devices.filter(device => device.status === 'active');
|
|
|
|
// 应用在线设备过滤
|
|
if (onlineDevicesOnly) {
|
|
devices = devices.filter(device => device.status === 'online');
|
|
}
|
|
|
|
// 应用过滤
|
|
if (deviceFilters.name) {
|
|
devices = devices.filter(device =>
|
|
device.device_name.toLowerCase().includes(deviceFilters.name.toLowerCase())
|
|
);
|
|
}
|
|
|
|
if (deviceFilters.type) {
|
|
devices = devices.filter(device =>
|
|
device.device_type === deviceFilters.type
|
|
);
|
|
}
|
|
|
|
if (deviceFilters.minChannels) {
|
|
devices = devices.filter(device =>
|
|
device.device_number >= parseInt(deviceFilters.minChannels)
|
|
);
|
|
}
|
|
|
|
if (deviceFilters.maxChannels) {
|
|
devices = devices.filter(device =>
|
|
device.device_number <= parseInt(deviceFilters.maxChannels)
|
|
);
|
|
}
|
|
|
|
// 应用排序
|
|
devices.sort((a, b) => {
|
|
const aValue = a[deviceSort.field];
|
|
const bValue = b[deviceSort.field];
|
|
|
|
if (deviceSort.order === 'asc') {
|
|
return aValue > bValue ? 1 : -1;
|
|
} else {
|
|
return aValue < bValue ? 1 : -1;
|
|
}
|
|
});
|
|
|
|
// 渲染设备列表
|
|
const devicesList = document.getElementById('devicesList');
|
|
if (devices.length === 0) {
|
|
devicesList.innerHTML = `
|
|
<div class="col-12 text-center">
|
|
<p class="text-muted">No devices found</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
devicesList.innerHTML = devices.map(device => `
|
|
<div class="col-md-4 mb-3">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h5 class="card-title mb-0">${device.device_name}</h5>
|
|
<span class="device-status ${device.status || 'offline'}"></span>
|
|
</div>
|
|
<p class="card-text">
|
|
Serial Number: ${device.serial_number}<br>
|
|
Type: ${device.device_type}<br>
|
|
Channel Count: ${device.device_number}
|
|
</p>
|
|
<!-- 添加传感器列表 -->
|
|
<div class="mt-3">
|
|
<h6 class="mb-2">Sensors:</h6>
|
|
${device.sensors && device.sensors.length > 0 ? `
|
|
<div class="table-responsive">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Index</th>
|
|
<th>Name</th>
|
|
<th>Type</th>
|
|
<th>Unit</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${device.sensors.map(sensor => `
|
|
<tr>
|
|
<td>${sensor.index}</td>
|
|
<td>${sensor.sensor_name}</td>
|
|
<td>${sensor.sensor_type}</td>
|
|
<td>${sensor.unit}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
` : '<p class="text-muted">No sensors available</p>'}
|
|
</div>
|
|
<div class="d-flex gap-2 mt-3">
|
|
<button class="btn btn-sm btn-danger" onclick="removeDevice('${device.user_device_id}')">
|
|
<i class="bi bi-trash"></i>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load device list:', error);
|
|
alert('Failed to load device list, please try again');
|
|
}
|
|
}
|
|
|
|
// 修改移除设备的函数
|
|
async function removeDevice(deviceId) {
|
|
if (!confirm('Are you sure you want to deactivate this device?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/user/devices/${deviceId}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
// 检查返回的状态
|
|
if (data.status === 'inactive') {
|
|
alert('Device deactivated successfully');
|
|
// 重新加载设备列表
|
|
loadDevices();
|
|
} else {
|
|
alert('Failed to deactivate device');
|
|
}
|
|
} else {
|
|
alert(`Failed: ${data.detail}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to remove device:', error);
|
|
alert('Failed to remove device, please try again');
|
|
}
|
|
}
|
|
|
|
// Modify add device function
|
|
async function addDevice() {
|
|
const serialNumber = document.getElementById('deviceId').value.trim();
|
|
|
|
if (!serialNumber) {
|
|
alert('Please enter device serial number');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/user/devices/${serialNumber}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
alert('Device added successfully!');
|
|
bootstrap.Modal.getInstance(document.getElementById('createDeviceModal')).hide();
|
|
document.getElementById('createDeviceForm').reset();
|
|
loadDevices();
|
|
} else {
|
|
alert(`Failed: ${data.detail}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to add device:', error);
|
|
alert('Failed to add device, please try again');
|
|
}
|
|
}
|
|
|
|
// 显示所有设备列表
|
|
function showDeviceList() {
|
|
// 重置过滤条件
|
|
deviceFilters = {
|
|
name: '',
|
|
type: '',
|
|
minChannels: '',
|
|
maxChannels: ''
|
|
};
|
|
|
|
// 重置在线设备过滤
|
|
onlineDevicesOnly = false;
|
|
|
|
// 移除所有按钮的激活状态
|
|
document.querySelectorAll('.sidebar-item').forEach(item => {
|
|
item.classList.remove('active');
|
|
});
|
|
|
|
// 激活Device List按钮
|
|
const deviceListButton = document.querySelector('.sidebar-item[onclick="showDeviceList()"]');
|
|
deviceListButton.classList.add('active');
|
|
|
|
// 重新加载设备列表
|
|
loadDevices();
|
|
}
|
|
|
|
// 页面加载完成时默认显示Device List
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
if (!token) {
|
|
alert('Please login first');
|
|
window.location.href = 'lab.html';
|
|
return;
|
|
}
|
|
// 显示Device List
|
|
showDeviceList();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |