Compare commits
	
		
			2 Commits
		
	
	
		
			7819773562
			...
			bbda4f4fca
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| bbda4f4fca | |||
| 9d36ae6b00 | 
							
								
								
									
										6
									
								
								TODO-List
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								TODO-List
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					// TODO 列表
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. websocket不是安全的wss
 | 
				
			||||||
 | 
					2. 添加设备时应该激活一下设备状态采集
 | 
				
			||||||
 | 
					3. 设备Model缺少硬件地址
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1,242 +0,0 @@
 | 
				
			|||||||
<!DOCTYPE html>
 | 
					 | 
				
			||||||
<html lang="zh-CN">
 | 
					 | 
				
			||||||
<head>
 | 
					 | 
				
			||||||
    <meta charset="UTF-8">
 | 
					 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
					 | 
				
			||||||
    <title>猪场管理系统 - 控制台</title>
 | 
					 | 
				
			||||||
    <style>
 | 
					 | 
				
			||||||
        * {
 | 
					 | 
				
			||||||
            margin: 0;
 | 
					 | 
				
			||||||
            padding: 0;
 | 
					 | 
				
			||||||
            box-sizing: border-box;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        body {
 | 
					 | 
				
			||||||
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 | 
					 | 
				
			||||||
            background: #f5f7fa;
 | 
					 | 
				
			||||||
            min-height: 100vh;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .header {
 | 
					 | 
				
			||||||
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | 
					 | 
				
			||||||
            color: white;
 | 
					 | 
				
			||||||
            padding: 20px;
 | 
					 | 
				
			||||||
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            justify-content: space-between;
 | 
					 | 
				
			||||||
            align-items: center;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .header h1 {
 | 
					 | 
				
			||||||
            font-size: 24px;
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            align-items: center;
 | 
					 | 
				
			||||||
            gap: 10px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .user-info {
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            align-items: center;
 | 
					 | 
				
			||||||
            gap: 15px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .logout-btn {
 | 
					 | 
				
			||||||
            background: rgba(255, 255, 255, 0.2);
 | 
					 | 
				
			||||||
            border: 1px solid rgba(255, 255, 255, 0.3);
 | 
					 | 
				
			||||||
            color: white;
 | 
					 | 
				
			||||||
            padding: 8px 15px;
 | 
					 | 
				
			||||||
            border-radius: 5px;
 | 
					 | 
				
			||||||
            cursor: pointer;
 | 
					 | 
				
			||||||
            transition: all 0.3s ease;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .logout-btn:hover {
 | 
					 | 
				
			||||||
            background: rgba(255, 255, 255, 0.3);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .container {
 | 
					 | 
				
			||||||
            max-width: 1200px;
 | 
					 | 
				
			||||||
            margin: 30px auto;
 | 
					 | 
				
			||||||
            padding: 0 20px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .dashboard-grid {
 | 
					 | 
				
			||||||
            display: grid;
 | 
					 | 
				
			||||||
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
 | 
					 | 
				
			||||||
            gap: 20px;
 | 
					 | 
				
			||||||
            margin-bottom: 30px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .card {
 | 
					 | 
				
			||||||
            background: white;
 | 
					 | 
				
			||||||
            border-radius: 15px;
 | 
					 | 
				
			||||||
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
 | 
					 | 
				
			||||||
            padding: 25px;
 | 
					 | 
				
			||||||
            transition: transform 0.3s ease, box-shadow 0.3s ease;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .card:hover {
 | 
					 | 
				
			||||||
            transform: translateY(-5px);
 | 
					 | 
				
			||||||
            box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .card h3 {
 | 
					 | 
				
			||||||
            color: #333;
 | 
					 | 
				
			||||||
            margin-bottom: 15px;
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            align-items: center;
 | 
					 | 
				
			||||||
            gap: 10px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .card p {
 | 
					 | 
				
			||||||
            color: #666;
 | 
					 | 
				
			||||||
            line-height: 1.6;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .device-control {
 | 
					 | 
				
			||||||
            background: white;
 | 
					 | 
				
			||||||
            border-radius: 15px;
 | 
					 | 
				
			||||||
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
 | 
					 | 
				
			||||||
            padding: 25px;
 | 
					 | 
				
			||||||
            margin-bottom: 30px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .control-grid {
 | 
					 | 
				
			||||||
            display: grid;
 | 
					 | 
				
			||||||
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
 | 
					 | 
				
			||||||
            gap: 20px;
 | 
					 | 
				
			||||||
            margin-top: 20px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .control-item {
 | 
					 | 
				
			||||||
            background: #f8f9fa;
 | 
					 | 
				
			||||||
            border-radius: 10px;
 | 
					 | 
				
			||||||
            padding: 20px;
 | 
					 | 
				
			||||||
            text-align: center;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .control-item h4 {
 | 
					 | 
				
			||||||
            margin-bottom: 15px;
 | 
					 | 
				
			||||||
            color: #333;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .control-buttons {
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            gap: 10px;
 | 
					 | 
				
			||||||
            justify-content: center;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .control-btn {
 | 
					 | 
				
			||||||
            padding: 8px 15px;
 | 
					 | 
				
			||||||
            border: none;
 | 
					 | 
				
			||||||
            border-radius: 5px;
 | 
					 | 
				
			||||||
            cursor: pointer;
 | 
					 | 
				
			||||||
            font-size: 14px;
 | 
					 | 
				
			||||||
            transition: all 0.3s ease;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .on-btn {
 | 
					 | 
				
			||||||
            background: #28a745;
 | 
					 | 
				
			||||||
            color: white;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .off-btn {
 | 
					 | 
				
			||||||
            background: #dc3545;
 | 
					 | 
				
			||||||
            color: white;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .control-btn:hover {
 | 
					 | 
				
			||||||
            opacity: 0.9;
 | 
					 | 
				
			||||||
            transform: translateY(-2px);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        @media (max-width: 768px) {
 | 
					 | 
				
			||||||
            .dashboard-grid {
 | 
					 | 
				
			||||||
                grid-template-columns: 1fr;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            .header {
 | 
					 | 
				
			||||||
                flex-direction: column;
 | 
					 | 
				
			||||||
                gap: 15px;
 | 
					 | 
				
			||||||
                text-align: center;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    </style>
 | 
					 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
<body>
 | 
					 | 
				
			||||||
    <div class="header">
 | 
					 | 
				
			||||||
        <h1>🐷 猪场管理系统</h1>
 | 
					 | 
				
			||||||
        <div class="user-info">
 | 
					 | 
				
			||||||
            <span id="username">用户</span>
 | 
					 | 
				
			||||||
            <button class="logout-btn" id="logoutBtn">退出登录</button>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <!-- 移除了导航栏 -->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="container">
 | 
					 | 
				
			||||||
        <div class="dashboard-grid">
 | 
					 | 
				
			||||||
            <div class="card">
 | 
					 | 
				
			||||||
                <h3>📊 系统状态</h3>
 | 
					 | 
				
			||||||
                <p>当前系统运行正常</p>
 | 
					 | 
				
			||||||
                <p>连接设备: <span id="deviceCount">0</span> 台</p>
 | 
					 | 
				
			||||||
                <p>今日操作: <span id="operationCount">0</span> 次</p>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <div class="card">
 | 
					 | 
				
			||||||
                <h3>🌡️ 环境监控</h3>
 | 
					 | 
				
			||||||
                <p>温度: <span id="temperature">25.6</span>°C</p>
 | 
					 | 
				
			||||||
                <p>湿度: <span id="humidity">65</span>%</p>
 | 
					 | 
				
			||||||
                <p>空气质量: 良好</p>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <div class="card">
 | 
					 | 
				
			||||||
                <h3>📡 设备连接</h3>
 | 
					 | 
				
			||||||
                <p>中继设备: <span class="status-indicator status-online"></span>在线</p>
 | 
					 | 
				
			||||||
                <p>风扇设备: <span class="status-indicator status-online"></span>在线</p>
 | 
					 | 
				
			||||||
                <p>水帘设备: <span class="status-indicator status-offline"></span>离线</p>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div class="device-control">
 | 
					 | 
				
			||||||
            <h2>⚙️ 设备控制</h2>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <script>
 | 
					 | 
				
			||||||
        // 检查用户是否已登录
 | 
					 | 
				
			||||||
        document.addEventListener('DOMContentLoaded', function() {
 | 
					 | 
				
			||||||
            const token = localStorage.getItem('authToken');
 | 
					 | 
				
			||||||
            const username = localStorage.getItem('username');
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            if (!token) {
 | 
					 | 
				
			||||||
                // 未登录,跳转到登录页面
 | 
					 | 
				
			||||||
                window.location.href = '/';
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            // 显示用户名
 | 
					 | 
				
			||||||
            if (username) {
 | 
					 | 
				
			||||||
                document.getElementById('username').textContent = username;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 退出登录
 | 
					 | 
				
			||||||
        function logout() {
 | 
					 | 
				
			||||||
            // 清除本地存储的认证信息
 | 
					 | 
				
			||||||
            localStorage.removeItem('authToken');
 | 
					 | 
				
			||||||
            localStorage.removeItem('userId');
 | 
					 | 
				
			||||||
            localStorage.removeItem('username');
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            // 跳转到登录页面
 | 
					 | 
				
			||||||
            window.location.href = '/';
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 控制设备
 | 
					 | 
				
			||||||
        function controlDevice(deviceType, action) {
 | 
					 | 
				
			||||||
            alert(`正在${action === 'on' ? '开启' : '关闭'}${deviceType === 'fan' ? '风机' : '水帘'}`);
 | 
					 | 
				
			||||||
            // 这里应该调用实际的设备控制API
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    </script>
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
</html>
 | 
					 | 
				
			||||||
@@ -1,812 +0,0 @@
 | 
				
			|||||||
<!DOCTYPE html>
 | 
					 | 
				
			||||||
<html lang="zh-CN">
 | 
					 | 
				
			||||||
<head>
 | 
					 | 
				
			||||||
    <meta charset="UTF-8">
 | 
					 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
					 | 
				
			||||||
    <title>猪场管理系统 - 设备管理</title>
 | 
					 | 
				
			||||||
    <style>
 | 
					 | 
				
			||||||
        * {
 | 
					 | 
				
			||||||
            margin: 0;
 | 
					 | 
				
			||||||
            padding: 0;
 | 
					 | 
				
			||||||
            box-sizing: border-box;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        body {
 | 
					 | 
				
			||||||
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 | 
					 | 
				
			||||||
            background: #f5f7fa;
 | 
					 | 
				
			||||||
            min-height: 100vh;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .header {
 | 
					 | 
				
			||||||
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | 
					 | 
				
			||||||
            color: white;
 | 
					 | 
				
			||||||
            padding: 20px;
 | 
					 | 
				
			||||||
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            justify-content: space-between;
 | 
					 | 
				
			||||||
            align-items: center;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .header h1 {
 | 
					 | 
				
			||||||
            font-size: 24px;
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            align-items: center;
 | 
					 | 
				
			||||||
            gap: 10px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .user-info {
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            align-items: center;
 | 
					 | 
				
			||||||
            gap: 15px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .logout-btn {
 | 
					 | 
				
			||||||
            background: rgba(255, 255, 255, 0.2);
 | 
					 | 
				
			||||||
            border: 1px solid rgba(255, 255, 255, 0.3);
 | 
					 | 
				
			||||||
            color: white;
 | 
					 | 
				
			||||||
            padding: 8px 15px;
 | 
					 | 
				
			||||||
            border-radius: 5px;
 | 
					 | 
				
			||||||
            cursor: pointer;
 | 
					 | 
				
			||||||
            transition: all 0.3s ease;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .logout-btn:hover {
 | 
					 | 
				
			||||||
            background: rgba(255, 255, 255, 0.3);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .nav {
 | 
					 | 
				
			||||||
            background: white;
 | 
					 | 
				
			||||||
            padding: 15px 20px;
 | 
					 | 
				
			||||||
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .nav ul {
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            list-style: none;
 | 
					 | 
				
			||||||
            gap: 20px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .nav a {
 | 
					 | 
				
			||||||
            text-decoration: none;
 | 
					 | 
				
			||||||
            color: #666;
 | 
					 | 
				
			||||||
            padding: 8px 15px;
 | 
					 | 
				
			||||||
            border-radius: 5px;
 | 
					 | 
				
			||||||
            transition: all 0.3s ease;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .nav a:hover, .nav a.active {
 | 
					 | 
				
			||||||
            background: #667eea;
 | 
					 | 
				
			||||||
            color: white;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .container {
 | 
					 | 
				
			||||||
            max-width: 1200px;
 | 
					 | 
				
			||||||
            margin: 30px auto;
 | 
					 | 
				
			||||||
            padding: 0 20px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .page-header {
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            justify-content: space-between;
 | 
					 | 
				
			||||||
            align-items: center;
 | 
					 | 
				
			||||||
            margin-bottom: 20px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .page-header h2 {
 | 
					 | 
				
			||||||
            color: #333;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .btn {
 | 
					 | 
				
			||||||
            padding: 10px 20px;
 | 
					 | 
				
			||||||
            border: none;
 | 
					 | 
				
			||||||
            border-radius: 5px;
 | 
					 | 
				
			||||||
            cursor: pointer;
 | 
					 | 
				
			||||||
            font-size: 14px;
 | 
					 | 
				
			||||||
            transition: all 0.3s ease;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .btn-primary {
 | 
					 | 
				
			||||||
            background: #667eea;
 | 
					 | 
				
			||||||
            color: white;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .btn-primary:hover {
 | 
					 | 
				
			||||||
            background: #5a6fd8;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .device-tree {
 | 
					 | 
				
			||||||
            background: white;
 | 
					 | 
				
			||||||
            border-radius: 15px;
 | 
					 | 
				
			||||||
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
 | 
					 | 
				
			||||||
            padding: 25px;
 | 
					 | 
				
			||||||
            margin-bottom: 30px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .tree-node {
 | 
					 | 
				
			||||||
            margin-bottom: 15px;
 | 
					 | 
				
			||||||
            border-left: 2px solid #e1e1e1;
 | 
					 | 
				
			||||||
            padding-left: 20px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .relay-node {
 | 
					 | 
				
			||||||
            border-left-color: #667eea;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .controller-node {
 | 
					 | 
				
			||||||
            border-left-color: #764ba2;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .device-node {
 | 
					 | 
				
			||||||
            border-left-color: #ffa500;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .node-header {
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            justify-content: space-between;
 | 
					 | 
				
			||||||
            align-items: center;
 | 
					 | 
				
			||||||
            padding: 10px;
 | 
					 | 
				
			||||||
            background: #f8f9fa;
 | 
					 | 
				
			||||||
            border-radius: 5px;
 | 
					 | 
				
			||||||
            margin-bottom: 10px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .relay-node > .node-header {
 | 
					 | 
				
			||||||
            background: rgba(102, 126, 234, 0.1);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .controller-node > .node-header {
 | 
					 | 
				
			||||||
            background: rgba(118, 75, 162, 0.1);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .device-node > .node-header {
 | 
					 | 
				
			||||||
            background: rgba(255, 165, 0, 0.1);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .node-title {
 | 
					 | 
				
			||||||
            font-weight: bold;
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            align-items: center;
 | 
					 | 
				
			||||||
            gap: 10px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .node-type {
 | 
					 | 
				
			||||||
            font-size: 12px;
 | 
					 | 
				
			||||||
            padding: 2px 8px;
 | 
					 | 
				
			||||||
            border-radius: 10px;
 | 
					 | 
				
			||||||
            color: white;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .relay-type {
 | 
					 | 
				
			||||||
            background: #667eea;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .controller-type {
 | 
					 | 
				
			||||||
            background: #764ba2;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .device-type {
 | 
					 | 
				
			||||||
            background: #ffa500;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .node-actions {
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            gap: 10px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .action-btn {
 | 
					 | 
				
			||||||
            padding: 5px 10px;
 | 
					 | 
				
			||||||
            border: none;
 | 
					 | 
				
			||||||
            border-radius: 3px;
 | 
					 | 
				
			||||||
            cursor: pointer;
 | 
					 | 
				
			||||||
            font-size: 12px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .edit-btn {
 | 
					 | 
				
			||||||
            background: #ffc107;
 | 
					 | 
				
			||||||
            color: #333;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .delete-btn {
 | 
					 | 
				
			||||||
            background: #dc3545;
 | 
					 | 
				
			||||||
            color: white;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .children {
 | 
					 | 
				
			||||||
            margin-left: 20px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .modal {
 | 
					 | 
				
			||||||
            display: none;
 | 
					 | 
				
			||||||
            position: fixed;
 | 
					 | 
				
			||||||
            top: 0;
 | 
					 | 
				
			||||||
            left: 0;
 | 
					 | 
				
			||||||
            width: 100%;
 | 
					 | 
				
			||||||
            height: 100%;
 | 
					 | 
				
			||||||
            background: rgba(0, 0, 0, 0.5);
 | 
					 | 
				
			||||||
            z-index: 1000;
 | 
					 | 
				
			||||||
            justify-content: center;
 | 
					 | 
				
			||||||
            align-items: center;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .modal-content {
 | 
					 | 
				
			||||||
            background: white;
 | 
					 | 
				
			||||||
            border-radius: 15px;
 | 
					 | 
				
			||||||
            width: 100%;
 | 
					 | 
				
			||||||
            max-width: 500px;
 | 
					 | 
				
			||||||
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .modal-header {
 | 
					 | 
				
			||||||
            padding: 20px;
 | 
					 | 
				
			||||||
            border-bottom: 1px solid #eee;
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            justify-content: space-between;
 | 
					 | 
				
			||||||
            align-items: center;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .modal-header h3 {
 | 
					 | 
				
			||||||
            margin: 0;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .close-btn {
 | 
					 | 
				
			||||||
            background: none;
 | 
					 | 
				
			||||||
            border: none;
 | 
					 | 
				
			||||||
            font-size: 24px;
 | 
					 | 
				
			||||||
            cursor: pointer;
 | 
					 | 
				
			||||||
            color: #999;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .modal-body {
 | 
					 | 
				
			||||||
            padding: 20px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .form-group {
 | 
					 | 
				
			||||||
            margin-bottom: 20px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .form-group label {
 | 
					 | 
				
			||||||
            display: block;
 | 
					 | 
				
			||||||
            margin-bottom: 5px;
 | 
					 | 
				
			||||||
            font-weight: 500;
 | 
					 | 
				
			||||||
            color: #333;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .form-group input, .form-group select {
 | 
					 | 
				
			||||||
            width: 100%;
 | 
					 | 
				
			||||||
            padding: 10px;
 | 
					 | 
				
			||||||
            border: 1px solid #ddd;
 | 
					 | 
				
			||||||
            border-radius: 5px;
 | 
					 | 
				
			||||||
            font-size: 14px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .modal-footer {
 | 
					 | 
				
			||||||
            padding: 20px;
 | 
					 | 
				
			||||||
            border-top: 1px solid #eee;
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            justify-content: flex-end;
 | 
					 | 
				
			||||||
            gap: 10px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .btn-secondary {
 | 
					 | 
				
			||||||
            background: #6c757d;
 | 
					 | 
				
			||||||
            color: white;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .status-active {
 | 
					 | 
				
			||||||
            color: #28a745;
 | 
					 | 
				
			||||||
            font-weight: bold;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .status-inactive {
 | 
					 | 
				
			||||||
            color: #dc3545;
 | 
					 | 
				
			||||||
            font-weight: bold;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        @media (max-width: 768px) {
 | 
					 | 
				
			||||||
            .dashboard-grid {
 | 
					 | 
				
			||||||
                grid-template-columns: 1fr;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            .header {
 | 
					 | 
				
			||||||
                flex-direction: column;
 | 
					 | 
				
			||||||
                gap: 15px;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            .page-header {
 | 
					 | 
				
			||||||
                flex-direction: column;
 | 
					 | 
				
			||||||
                gap: 15px;
 | 
					 | 
				
			||||||
                align-items: flex-start;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    </style>
 | 
					 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
<body>
 | 
					 | 
				
			||||||
    <div class="header">
 | 
					 | 
				
			||||||
        <h1>🐷 猪场管理系统</h1>
 | 
					 | 
				
			||||||
        <div class="user-info">
 | 
					 | 
				
			||||||
            <span id="username">管理员</span>
 | 
					 | 
				
			||||||
            <button class="logout-btn" onclick="logout()">退出登录</button>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="nav">
 | 
					 | 
				
			||||||
        <ul>
 | 
					 | 
				
			||||||
            <li><a href="dashboard.html">控制台</a></li>
 | 
					 | 
				
			||||||
            <li><a href="device.html" class="active">设备管理</a></li>
 | 
					 | 
				
			||||||
        </ul>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="container">
 | 
					 | 
				
			||||||
        <div class="page-header">
 | 
					 | 
				
			||||||
            <h2>设备管理</h2>
 | 
					 | 
				
			||||||
            <button class="btn btn-primary" onclick="openAddDeviceModal()">添加设备</button>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div class="device-tree" id="deviceTree">
 | 
					 | 
				
			||||||
            <!-- 设备树将通过JavaScript动态生成 -->
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <!-- 添加/编辑设备模态框 -->
 | 
					 | 
				
			||||||
    <div class="modal" id="deviceModal">
 | 
					 | 
				
			||||||
        <div class="modal-content">
 | 
					 | 
				
			||||||
            <div class="modal-header">
 | 
					 | 
				
			||||||
                <h3 id="modalTitle">添加设备</h3>
 | 
					 | 
				
			||||||
                <button class="close-btn" onclick="closeDeviceModal()">×</button>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div class="modal-body">
 | 
					 | 
				
			||||||
                <form id="deviceForm">
 | 
					 | 
				
			||||||
                    <input type="hidden" id="deviceId">
 | 
					 | 
				
			||||||
                    <div class="form-group">
 | 
					 | 
				
			||||||
                        <label for="deviceName">设备名称</label>
 | 
					 | 
				
			||||||
                        <input type="text" id="deviceName" required>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                    <div class="form-group">
 | 
					 | 
				
			||||||
                        <label for="deviceType">设备类型</label>
 | 
					 | 
				
			||||||
                        <select id="deviceType" required onchange="toggleParentField()">
 | 
					 | 
				
			||||||
                            <option value="">请选择设备类型</option>
 | 
					 | 
				
			||||||
                            <option value="relay">中继设备</option>
 | 
					 | 
				
			||||||
                            <option value="pig_pen_controller">猪舍主控</option>
 | 
					 | 
				
			||||||
                            <option value="feed_mill_controller">做料车间主控</option>
 | 
					 | 
				
			||||||
                            <option value="fan">风机</option>
 | 
					 | 
				
			||||||
                            <option value="water_curtain">水帘</option>
 | 
					 | 
				
			||||||
                        </select>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                    <div class="form-group" id="parentField" style="display: none;">
 | 
					 | 
				
			||||||
                        <label for="parentId">上级设备</label>
 | 
					 | 
				
			||||||
                        <select id="parentId">
 | 
					 | 
				
			||||||
                            <option value="">请选择上级设备</option>
 | 
					 | 
				
			||||||
                        </select>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                    <div class="form-group">
 | 
					 | 
				
			||||||
                        <label for="deviceStatus">设备状态</label>
 | 
					 | 
				
			||||||
                        <select id="deviceStatus" required>
 | 
					 | 
				
			||||||
                            <option value="active">启用</option>
 | 
					 | 
				
			||||||
                            <option value="inactive">停用</option>
 | 
					 | 
				
			||||||
                        </select>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                </form>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div class="modal-footer">
 | 
					 | 
				
			||||||
                <button class="btn btn-secondary" onclick="closeDeviceModal()">取消</button>
 | 
					 | 
				
			||||||
                <button class="btn btn-primary" onclick="saveDevice()">保存</button>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <script>
 | 
					 | 
				
			||||||
        // 检查用户是否已登录
 | 
					 | 
				
			||||||
        document.addEventListener('DOMContentLoaded', function() {
 | 
					 | 
				
			||||||
            const token = localStorage.getItem('authToken');
 | 
					 | 
				
			||||||
            const username = localStorage.getItem('username');
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            if (!token) {
 | 
					 | 
				
			||||||
                // 未登录,跳转到登录页面
 | 
					 | 
				
			||||||
                window.location.href = '/';
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            // 显示用户名
 | 
					 | 
				
			||||||
            if (username) {
 | 
					 | 
				
			||||||
                document.getElementById('username').textContent = username;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            // 加载设备列表
 | 
					 | 
				
			||||||
            loadDevices();
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 当前设备列表
 | 
					 | 
				
			||||||
        let devices = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 获取设备列表
 | 
					 | 
				
			||||||
        function loadDevices() {
 | 
					 | 
				
			||||||
            // 这里应该调用后端API获取设备列表
 | 
					 | 
				
			||||||
            // 暂时使用模拟数据
 | 
					 | 
				
			||||||
            fetch('/api/v1/device/list')
 | 
					 | 
				
			||||||
                .then(response => response.json())
 | 
					 | 
				
			||||||
                .then(data => {
 | 
					 | 
				
			||||||
                    if (data.code === 0) {
 | 
					 | 
				
			||||||
                        devices = data.data.devices;
 | 
					 | 
				
			||||||
                        renderDeviceTree();
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        // 如果无法获取数据,使用模拟数据
 | 
					 | 
				
			||||||
                        mockDevices();
 | 
					 | 
				
			||||||
                        renderDeviceTree();
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .catch(error => {
 | 
					 | 
				
			||||||
                    console.error('获取设备列表失败:', error);
 | 
					 | 
				
			||||||
                    // 如果无法获取数据,使用模拟数据
 | 
					 | 
				
			||||||
                    mockDevices();
 | 
					 | 
				
			||||||
                    renderDeviceTree();
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 模拟设备数据
 | 
					 | 
				
			||||||
        function mockDevices() {
 | 
					 | 
				
			||||||
            devices = [
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    id: 1,
 | 
					 | 
				
			||||||
                    name: "一号中继器",
 | 
					 | 
				
			||||||
                    type: "relay",
 | 
					 | 
				
			||||||
                    parent_id: null,
 | 
					 | 
				
			||||||
                    status: "active",
 | 
					 | 
				
			||||||
                    created_at: "2023-01-01T00:00:00Z"
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    id: 2,
 | 
					 | 
				
			||||||
                    name: "A区主控",
 | 
					 | 
				
			||||||
                    type: "pig_pen_controller",
 | 
					 | 
				
			||||||
                    parent_id: 1,
 | 
					 | 
				
			||||||
                    status: "active",
 | 
					 | 
				
			||||||
                    created_at: "2023-01-02T00:00:00Z"
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    id: 3,
 | 
					 | 
				
			||||||
                    name: "A区1号风机",
 | 
					 | 
				
			||||||
                    type: "fan",
 | 
					 | 
				
			||||||
                    parent_id: 2,
 | 
					 | 
				
			||||||
                    status: "active",
 | 
					 | 
				
			||||||
                    created_at: "2023-01-03T00:00:00Z"
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    id: 4,
 | 
					 | 
				
			||||||
                    name: "A区2号风机",
 | 
					 | 
				
			||||||
                    type: "fan",
 | 
					 | 
				
			||||||
                    parent_id: 2,
 | 
					 | 
				
			||||||
                    status: "inactive",
 | 
					 | 
				
			||||||
                    created_at: "2023-01-03T00:00:00Z"
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    id: 5,
 | 
					 | 
				
			||||||
                    name: "B区主控",
 | 
					 | 
				
			||||||
                    type: "pig_pen_controller",
 | 
					 | 
				
			||||||
                    parent_id: 1,
 | 
					 | 
				
			||||||
                    status: "active",
 | 
					 | 
				
			||||||
                    created_at: "2023-01-02T00:00:00Z"
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    id: 6,
 | 
					 | 
				
			||||||
                    name: "B区水帘",
 | 
					 | 
				
			||||||
                    type: "water_curtain",
 | 
					 | 
				
			||||||
                    parent_id: 5,
 | 
					 | 
				
			||||||
                    status: "active",
 | 
					 | 
				
			||||||
                    created_at: "2023-01-04T00:00:00Z"
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            ];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 渲染设备树
 | 
					 | 
				
			||||||
        function renderDeviceTree() {
 | 
					 | 
				
			||||||
            const treeContainer = document.getElementById('deviceTree');
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            // 获取所有中继设备(顶级节点)
 | 
					 | 
				
			||||||
            const relays = devices.filter(device => device.type === 'relay');
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            let treeHTML = '';
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            relays.forEach(relay => {
 | 
					 | 
				
			||||||
                treeHTML += renderRelayNode(relay);
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            treeContainer.innerHTML = treeHTML || '<p>暂无设备数据</p>';
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 渲染中继节点
 | 
					 | 
				
			||||||
        function renderRelayNode(relay) {
 | 
					 | 
				
			||||||
            const controllers = devices.filter(device => device.parent_id === relay.id);
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            let html = `
 | 
					 | 
				
			||||||
                <div class="tree-node relay-node">
 | 
					 | 
				
			||||||
                    <div class="node-header">
 | 
					 | 
				
			||||||
                        <div class="node-title">
 | 
					 | 
				
			||||||
                            <span>${relay.name}</span>
 | 
					 | 
				
			||||||
                            <span class="node-type relay-type">中继</span>
 | 
					 | 
				
			||||||
                            <span class="status-${relay.status}">${relay.status === 'active' ? '启用' : '停用'}</span>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                        <div class="node-actions">
 | 
					 | 
				
			||||||
                            <button class="action-btn edit-btn" onclick="editDevice(${relay.id})">编辑</button>
 | 
					 | 
				
			||||||
                            <button class="action-btn delete-btn" onclick="deleteDevice(${relay.id})">删除</button>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
            `;
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            if (controllers.length > 0) {
 | 
					 | 
				
			||||||
                html += '<div class="children">';
 | 
					 | 
				
			||||||
                controllers.forEach(controller => {
 | 
					 | 
				
			||||||
                    html += renderControllerNode(controller);
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
                html += '</div>';
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            html += '</div>';
 | 
					 | 
				
			||||||
            return html;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 渲染控制器节点
 | 
					 | 
				
			||||||
        function renderControllerNode(controller) {
 | 
					 | 
				
			||||||
            const childDevices = devices.filter(device => device.parent_id === controller.id);
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            let typeText = '';
 | 
					 | 
				
			||||||
            switch(controller.type) {
 | 
					 | 
				
			||||||
                case 'pig_pen_controller':
 | 
					 | 
				
			||||||
                    typeText = '猪舍主控';
 | 
					 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
                case 'feed_mill_controller':
 | 
					 | 
				
			||||||
                    typeText = '做料车间主控';
 | 
					 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
                default:
 | 
					 | 
				
			||||||
                    typeText = controller.type;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            let html = `
 | 
					 | 
				
			||||||
                <div class="tree-node controller-node">
 | 
					 | 
				
			||||||
                    <div class="node-header">
 | 
					 | 
				
			||||||
                        <div class="node-title">
 | 
					 | 
				
			||||||
                            <span>${controller.name}</span>
 | 
					 | 
				
			||||||
                            <span class="node-type controller-type">${typeText}</span>
 | 
					 | 
				
			||||||
                            <span class="status-${controller.status}">${controller.status === 'active' ? '启用' : '停用'}</span>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                        <div class="node-actions">
 | 
					 | 
				
			||||||
                            <button class="action-btn edit-btn" onclick="editDevice(${controller.id})">编辑</button>
 | 
					 | 
				
			||||||
                            <button class="action-btn delete-btn" onclick="deleteDevice(${controller.id})">删除</button>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
            `;
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            if (childDevices.length > 0) {
 | 
					 | 
				
			||||||
                html += '<div class="children">';
 | 
					 | 
				
			||||||
                childDevices.forEach(device => {
 | 
					 | 
				
			||||||
                    html += renderDeviceNode(device);
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
                html += '</div>';
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            html += '</div>';
 | 
					 | 
				
			||||||
            return html;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 渲染设备节点
 | 
					 | 
				
			||||||
        function renderDeviceNode(device) {
 | 
					 | 
				
			||||||
            let typeText = '';
 | 
					 | 
				
			||||||
            switch(device.type) {
 | 
					 | 
				
			||||||
                case 'fan':
 | 
					 | 
				
			||||||
                    typeText = '风机';
 | 
					 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
                case 'water_curtain':
 | 
					 | 
				
			||||||
                    typeText = '水帘';
 | 
					 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
                default:
 | 
					 | 
				
			||||||
                    typeText = device.type;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            return `
 | 
					 | 
				
			||||||
                <div class="tree-node device-node">
 | 
					 | 
				
			||||||
                    <div class="node-header">
 | 
					 | 
				
			||||||
                        <div class="node-title">
 | 
					 | 
				
			||||||
                            <span>${device.name}</span>
 | 
					 | 
				
			||||||
                            <span class="node-type device-type">${typeText}</span>
 | 
					 | 
				
			||||||
                            <span class="status-${device.status}">${device.status === 'active' ? '启用' : '停用'}</span>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                        <div class="node-actions">
 | 
					 | 
				
			||||||
                            <button class="action-btn edit-btn" onclick="editDevice(${device.id})">编辑</button>
 | 
					 | 
				
			||||||
                            <button class="action-btn delete-btn" onclick="deleteDevice(${device.id})">删除</button>
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            `;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 打开添加设备模态框
 | 
					 | 
				
			||||||
        function openAddDeviceModal() {
 | 
					 | 
				
			||||||
            document.getElementById('modalTitle').textContent = '添加设备';
 | 
					 | 
				
			||||||
            document.getElementById('deviceForm').reset();
 | 
					 | 
				
			||||||
            document.getElementById('deviceId').value = '';
 | 
					 | 
				
			||||||
            toggleParentField();
 | 
					 | 
				
			||||||
            loadParentDevices();
 | 
					 | 
				
			||||||
            document.getElementById('deviceModal').style.display = 'flex';
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 编辑设备
 | 
					 | 
				
			||||||
        function editDevice(deviceId) {
 | 
					 | 
				
			||||||
            const device = devices.find(d => d.id === deviceId);
 | 
					 | 
				
			||||||
            if (!device) return;
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            document.getElementById('modalTitle').textContent = '编辑设备';
 | 
					 | 
				
			||||||
            document.getElementById('deviceId').value = device.id;
 | 
					 | 
				
			||||||
            document.getElementById('deviceName').value = device.name;
 | 
					 | 
				
			||||||
            document.getElementById('deviceType').value = device.type;
 | 
					 | 
				
			||||||
            document.getElementById('deviceStatus').value = device.status;
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            toggleParentField();
 | 
					 | 
				
			||||||
            loadParentDevices(device.type);
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            if (device.parent_id) {
 | 
					 | 
				
			||||||
                document.getElementById('parentId').value = device.parent_id;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            document.getElementById('deviceModal').style.display = 'flex';
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 关闭模态框
 | 
					 | 
				
			||||||
        function closeDeviceModal() {
 | 
					 | 
				
			||||||
            document.getElementById('deviceModal').style.display = 'none';
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 根据设备类型切换上级设备字段显示
 | 
					 | 
				
			||||||
        function toggleParentField() {
 | 
					 | 
				
			||||||
            const type = document.getElementById('deviceType').value;
 | 
					 | 
				
			||||||
            const parentField = document.getElementById('parentField');
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            if (type === 'relay' || !type) {
 | 
					 | 
				
			||||||
                parentField.style.display = 'none';
 | 
					 | 
				
			||||||
                document.getElementById('parentId').innerHTML = '<option value="">请选择上级设备</option>';
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                parentField.style.display = 'block';
 | 
					 | 
				
			||||||
                loadParentDevices(type);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 加载上级设备选项
 | 
					 | 
				
			||||||
        function loadParentDevices(currentType) {
 | 
					 | 
				
			||||||
            const parentIdSelect = document.getElementById('parentId');
 | 
					 | 
				
			||||||
            parentIdSelect.innerHTML = '<option value="">请选择上级设备</option>';
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            let parentDevices = [];
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            if (currentType === 'pig_pen_controller' || currentType === 'feed_mill_controller') {
 | 
					 | 
				
			||||||
                // 控制器的上级是中继设备
 | 
					 | 
				
			||||||
                parentDevices = devices.filter(device => device.type === 'relay');
 | 
					 | 
				
			||||||
            } else if (currentType === 'fan' || currentType === 'water_curtain') {
 | 
					 | 
				
			||||||
                // 设备的上级是控制器
 | 
					 | 
				
			||||||
                parentDevices = devices.filter(device => 
 | 
					 | 
				
			||||||
                    device.type === 'pig_pen_controller' || device.type === 'feed_mill_controller');
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            parentDevices.forEach(device => {
 | 
					 | 
				
			||||||
                const option = document.createElement('option');
 | 
					 | 
				
			||||||
                option.value = device.id;
 | 
					 | 
				
			||||||
                option.textContent = device.name;
 | 
					 | 
				
			||||||
                parentIdSelect.appendChild(option);
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 保存设备
 | 
					 | 
				
			||||||
        function saveDevice() {
 | 
					 | 
				
			||||||
            const deviceId = document.getElementById('deviceId').value;
 | 
					 | 
				
			||||||
            const name = document.getElementById('deviceName').value;
 | 
					 | 
				
			||||||
            const type = document.getElementById('deviceType').value;
 | 
					 | 
				
			||||||
            const parentId = document.getElementById('parentId').value;
 | 
					 | 
				
			||||||
            const status = document.getElementById('deviceStatus').value;
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            if (!name || !type) {
 | 
					 | 
				
			||||||
                alert('请填写必填字段');
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            const deviceData = {
 | 
					 | 
				
			||||||
                name: name,
 | 
					 | 
				
			||||||
                type: type,
 | 
					 | 
				
			||||||
                status: status,
 | 
					 | 
				
			||||||
                parent_id: parentId ? parseInt(parentId) : null
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            if (deviceId) {
 | 
					 | 
				
			||||||
                // 编辑设备
 | 
					 | 
				
			||||||
                deviceData.id = parseInt(deviceId);
 | 
					 | 
				
			||||||
                fetch('/api/v1/device/update', {
 | 
					 | 
				
			||||||
                    method: 'POST',
 | 
					 | 
				
			||||||
                    headers: {
 | 
					 | 
				
			||||||
                        'Content-Type': 'application/json',
 | 
					 | 
				
			||||||
                        'Authorization': 'Bearer ' + localStorage.getItem('authToken')
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    body: JSON.stringify(deviceData)
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .then(response => response.json())
 | 
					 | 
				
			||||||
                .then(data => {
 | 
					 | 
				
			||||||
                    if (data.code === 0) {
 | 
					 | 
				
			||||||
                        loadDevices(); // 重新加载设备列表
 | 
					 | 
				
			||||||
                        closeDeviceModal();
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        alert('更新设备失败: ' + data.message);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .catch(error => {
 | 
					 | 
				
			||||||
                    console.error('更新设备失败:', error);
 | 
					 | 
				
			||||||
                    alert('更新设备失败,请查看控制台了解详情');
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                // 添加设备
 | 
					 | 
				
			||||||
                fetch('/api/v1/device/create', {
 | 
					 | 
				
			||||||
                    method: 'POST',
 | 
					 | 
				
			||||||
                    headers: {
 | 
					 | 
				
			||||||
                        'Content-Type': 'application/json',
 | 
					 | 
				
			||||||
                        'Authorization': 'Bearer ' + localStorage.getItem('authToken')
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    body: JSON.stringify(deviceData)
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .then(response => response.json())
 | 
					 | 
				
			||||||
                .then(data => {
 | 
					 | 
				
			||||||
                    if (data.code === 0) {
 | 
					 | 
				
			||||||
                        loadDevices(); // 重新加载设备列表
 | 
					 | 
				
			||||||
                        closeDeviceModal();
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        alert('创建设备失败: ' + data.message);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .catch(error => {
 | 
					 | 
				
			||||||
                    console.error('创建设备失败:', error);
 | 
					 | 
				
			||||||
                    alert('创建设备失败,请查看控制台了解详情');
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 删除设备
 | 
					 | 
				
			||||||
        function deleteDevice(deviceId) {
 | 
					 | 
				
			||||||
            if (!confirm('确定要删除这个设备吗?')) {
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            fetch('/api/v1/device/delete', {
 | 
					 | 
				
			||||||
                method: 'POST',
 | 
					 | 
				
			||||||
                headers: {
 | 
					 | 
				
			||||||
                    'Content-Type': 'application/json',
 | 
					 | 
				
			||||||
                    'Authorization': 'Bearer ' + localStorage.getItem('authToken')
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                body: JSON.stringify({id: deviceId})
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .then(response => response.json())
 | 
					 | 
				
			||||||
            .then(data => {
 | 
					 | 
				
			||||||
                if (data.code === 0) {
 | 
					 | 
				
			||||||
                    loadDevices(); // 重新加载设备列表
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    alert('删除设备失败: ' + data.message);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .catch(error => {
 | 
					 | 
				
			||||||
                console.error('删除设备失败:', error);
 | 
					 | 
				
			||||||
                alert('删除设备失败,请查看控制台了解详情');
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 退出登录
 | 
					 | 
				
			||||||
        function logout() {
 | 
					 | 
				
			||||||
            // 清除本地存储的认证信息
 | 
					 | 
				
			||||||
            localStorage.removeItem('authToken');
 | 
					 | 
				
			||||||
            localStorage.removeItem('userId');
 | 
					 | 
				
			||||||
            localStorage.removeItem('username');
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            // 跳转到登录页面
 | 
					 | 
				
			||||||
            window.location.href = '/';
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 点击模态框外部关闭模态框
 | 
					 | 
				
			||||||
        window.onclick = function(event) {
 | 
					 | 
				
			||||||
            const modal = document.getElementById('deviceModal');
 | 
					 | 
				
			||||||
            if (event.target === modal) {
 | 
					 | 
				
			||||||
                closeDeviceModal();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
    </script>
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
</html>
 | 
					 | 
				
			||||||
							
								
								
									
										1
									
								
								frontend/dist/assets/index.11ac5c94.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								frontend/dist/assets/index.11ac5c94.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										21
									
								
								frontend/dist/assets/index.3b53509d.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								frontend/dist/assets/index.3b53509d.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										21
									
								
								frontend/dist/assets/index.3d7f01fe.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								frontend/dist/assets/index.3d7f01fe.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								frontend/dist/assets/index.fd8bdce3.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/dist/assets/index.fd8bdce3.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										4
									
								
								frontend/dist/index.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								frontend/dist/index.html
									
									
									
									
										vendored
									
									
								
							@@ -4,8 +4,8 @@
 | 
				
			|||||||
    <meta charset="UTF-8">
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
    <title>猪场管理系统</title>
 | 
					    <title>猪场管理系统</title>
 | 
				
			||||||
  <script type="module" crossorigin src="/assets/index.3b53509d.js"></script>
 | 
					  <script type="module" crossorigin src="/assets/index.3d7f01fe.js"></script>
 | 
				
			||||||
  <link rel="stylesheet" href="/assets/index.11ac5c94.css">
 | 
					  <link rel="stylesheet" href="/assets/index.fd8bdce3.css">
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
    <div id="app"></div>
 | 
					    <div id="app"></div>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										23
									
								
								frontend/node_modules/.vite/deps_temp/_metadata.json
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								frontend/node_modules/.vite/deps_temp/_metadata.json
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "hash": "dd8a32db",
 | 
				
			||||||
 | 
					  "browserHash": "91f1c2e1",
 | 
				
			||||||
 | 
					  "optimized": {
 | 
				
			||||||
 | 
					    "vue": {
 | 
				
			||||||
 | 
					      "src": "../../vue/dist/vue.runtime.esm-bundler.js",
 | 
				
			||||||
 | 
					      "file": "vue.js",
 | 
				
			||||||
 | 
					      "fileHash": "6d85718e",
 | 
				
			||||||
 | 
					      "needsInterop": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "vue-router": {
 | 
				
			||||||
 | 
					      "src": "../../vue-router/dist/vue-router.mjs",
 | 
				
			||||||
 | 
					      "file": "vue-router.js",
 | 
				
			||||||
 | 
					      "fileHash": "1e430545",
 | 
				
			||||||
 | 
					      "needsInterop": false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "chunks": {
 | 
				
			||||||
 | 
					    "chunk-DB3RJHEA": {
 | 
				
			||||||
 | 
					      "file": "chunk-DB3RJHEA.js"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12441
									
								
								frontend/node_modules/.vite/deps_temp/chunk-DB3RJHEA.js
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12441
									
								
								frontend/node_modules/.vite/deps_temp/chunk-DB3RJHEA.js
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										7
									
								
								frontend/node_modules/.vite/deps_temp/chunk-DB3RJHEA.js.map
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/node_modules/.vite/deps_temp/chunk-DB3RJHEA.js.map
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								frontend/node_modules/.vite/deps_temp/package.json
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/node_modules/.vite/deps_temp/package.json
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					{"type":"module"}
 | 
				
			||||||
							
								
								
									
										2768
									
								
								frontend/node_modules/.vite/deps_temp/vue-router.js
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2768
									
								
								frontend/node_modules/.vite/deps_temp/vue-router.js
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										7
									
								
								frontend/node_modules/.vite/deps_temp/vue-router.js.map
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/node_modules/.vite/deps_temp/vue-router.js.map
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										343
									
								
								frontend/node_modules/.vite/deps_temp/vue.js
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										343
									
								
								frontend/node_modules/.vite/deps_temp/vue.js
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,343 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					  BaseTransition,
 | 
				
			||||||
 | 
					  BaseTransitionPropsValidators,
 | 
				
			||||||
 | 
					  Comment,
 | 
				
			||||||
 | 
					  DeprecationTypes,
 | 
				
			||||||
 | 
					  EffectScope,
 | 
				
			||||||
 | 
					  ErrorCodes,
 | 
				
			||||||
 | 
					  ErrorTypeStrings,
 | 
				
			||||||
 | 
					  Fragment,
 | 
				
			||||||
 | 
					  KeepAlive,
 | 
				
			||||||
 | 
					  ReactiveEffect,
 | 
				
			||||||
 | 
					  Static,
 | 
				
			||||||
 | 
					  Suspense,
 | 
				
			||||||
 | 
					  Teleport,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
 | 
					  TrackOpTypes,
 | 
				
			||||||
 | 
					  Transition,
 | 
				
			||||||
 | 
					  TransitionGroup,
 | 
				
			||||||
 | 
					  TriggerOpTypes,
 | 
				
			||||||
 | 
					  VueElement,
 | 
				
			||||||
 | 
					  assertNumber,
 | 
				
			||||||
 | 
					  callWithAsyncErrorHandling,
 | 
				
			||||||
 | 
					  callWithErrorHandling,
 | 
				
			||||||
 | 
					  camelize,
 | 
				
			||||||
 | 
					  capitalize,
 | 
				
			||||||
 | 
					  cloneVNode,
 | 
				
			||||||
 | 
					  compatUtils,
 | 
				
			||||||
 | 
					  compile,
 | 
				
			||||||
 | 
					  computed,
 | 
				
			||||||
 | 
					  createApp,
 | 
				
			||||||
 | 
					  createBaseVNode,
 | 
				
			||||||
 | 
					  createBlock,
 | 
				
			||||||
 | 
					  createCommentVNode,
 | 
				
			||||||
 | 
					  createElementBlock,
 | 
				
			||||||
 | 
					  createHydrationRenderer,
 | 
				
			||||||
 | 
					  createPropsRestProxy,
 | 
				
			||||||
 | 
					  createRenderer,
 | 
				
			||||||
 | 
					  createSSRApp,
 | 
				
			||||||
 | 
					  createSlots,
 | 
				
			||||||
 | 
					  createStaticVNode,
 | 
				
			||||||
 | 
					  createTextVNode,
 | 
				
			||||||
 | 
					  createVNode,
 | 
				
			||||||
 | 
					  customRef,
 | 
				
			||||||
 | 
					  defineAsyncComponent,
 | 
				
			||||||
 | 
					  defineComponent,
 | 
				
			||||||
 | 
					  defineCustomElement,
 | 
				
			||||||
 | 
					  defineEmits,
 | 
				
			||||||
 | 
					  defineExpose,
 | 
				
			||||||
 | 
					  defineModel,
 | 
				
			||||||
 | 
					  defineOptions,
 | 
				
			||||||
 | 
					  defineProps,
 | 
				
			||||||
 | 
					  defineSSRCustomElement,
 | 
				
			||||||
 | 
					  defineSlots,
 | 
				
			||||||
 | 
					  devtools,
 | 
				
			||||||
 | 
					  effect,
 | 
				
			||||||
 | 
					  effectScope,
 | 
				
			||||||
 | 
					  getCurrentInstance,
 | 
				
			||||||
 | 
					  getCurrentScope,
 | 
				
			||||||
 | 
					  getCurrentWatcher,
 | 
				
			||||||
 | 
					  getTransitionRawChildren,
 | 
				
			||||||
 | 
					  guardReactiveProps,
 | 
				
			||||||
 | 
					  h,
 | 
				
			||||||
 | 
					  handleError,
 | 
				
			||||||
 | 
					  hasInjectionContext,
 | 
				
			||||||
 | 
					  hydrate,
 | 
				
			||||||
 | 
					  hydrateOnIdle,
 | 
				
			||||||
 | 
					  hydrateOnInteraction,
 | 
				
			||||||
 | 
					  hydrateOnMediaQuery,
 | 
				
			||||||
 | 
					  hydrateOnVisible,
 | 
				
			||||||
 | 
					  initCustomFormatter,
 | 
				
			||||||
 | 
					  initDirectivesForSSR,
 | 
				
			||||||
 | 
					  inject,
 | 
				
			||||||
 | 
					  isMemoSame,
 | 
				
			||||||
 | 
					  isProxy,
 | 
				
			||||||
 | 
					  isReactive,
 | 
				
			||||||
 | 
					  isReadonly,
 | 
				
			||||||
 | 
					  isRef,
 | 
				
			||||||
 | 
					  isRuntimeOnly,
 | 
				
			||||||
 | 
					  isShallow,
 | 
				
			||||||
 | 
					  isVNode,
 | 
				
			||||||
 | 
					  markRaw,
 | 
				
			||||||
 | 
					  mergeDefaults,
 | 
				
			||||||
 | 
					  mergeModels,
 | 
				
			||||||
 | 
					  mergeProps,
 | 
				
			||||||
 | 
					  nextTick,
 | 
				
			||||||
 | 
					  normalizeClass,
 | 
				
			||||||
 | 
					  normalizeProps,
 | 
				
			||||||
 | 
					  normalizeStyle,
 | 
				
			||||||
 | 
					  onActivated,
 | 
				
			||||||
 | 
					  onBeforeMount,
 | 
				
			||||||
 | 
					  onBeforeUnmount,
 | 
				
			||||||
 | 
					  onBeforeUpdate,
 | 
				
			||||||
 | 
					  onDeactivated,
 | 
				
			||||||
 | 
					  onErrorCaptured,
 | 
				
			||||||
 | 
					  onMounted,
 | 
				
			||||||
 | 
					  onRenderTracked,
 | 
				
			||||||
 | 
					  onRenderTriggered,
 | 
				
			||||||
 | 
					  onScopeDispose,
 | 
				
			||||||
 | 
					  onServerPrefetch,
 | 
				
			||||||
 | 
					  onUnmounted,
 | 
				
			||||||
 | 
					  onUpdated,
 | 
				
			||||||
 | 
					  onWatcherCleanup,
 | 
				
			||||||
 | 
					  openBlock,
 | 
				
			||||||
 | 
					  popScopeId,
 | 
				
			||||||
 | 
					  provide,
 | 
				
			||||||
 | 
					  proxyRefs,
 | 
				
			||||||
 | 
					  pushScopeId,
 | 
				
			||||||
 | 
					  queuePostFlushCb,
 | 
				
			||||||
 | 
					  reactive,
 | 
				
			||||||
 | 
					  readonly,
 | 
				
			||||||
 | 
					  ref,
 | 
				
			||||||
 | 
					  registerRuntimeCompiler,
 | 
				
			||||||
 | 
					  render,
 | 
				
			||||||
 | 
					  renderList,
 | 
				
			||||||
 | 
					  renderSlot,
 | 
				
			||||||
 | 
					  resolveComponent,
 | 
				
			||||||
 | 
					  resolveDirective,
 | 
				
			||||||
 | 
					  resolveDynamicComponent,
 | 
				
			||||||
 | 
					  resolveFilter,
 | 
				
			||||||
 | 
					  resolveTransitionHooks,
 | 
				
			||||||
 | 
					  setBlockTracking,
 | 
				
			||||||
 | 
					  setDevtoolsHook,
 | 
				
			||||||
 | 
					  setTransitionHooks,
 | 
				
			||||||
 | 
					  shallowReactive,
 | 
				
			||||||
 | 
					  shallowReadonly,
 | 
				
			||||||
 | 
					  shallowRef,
 | 
				
			||||||
 | 
					  ssrContextKey,
 | 
				
			||||||
 | 
					  ssrUtils,
 | 
				
			||||||
 | 
					  stop,
 | 
				
			||||||
 | 
					  toDisplayString,
 | 
				
			||||||
 | 
					  toHandlerKey,
 | 
				
			||||||
 | 
					  toHandlers,
 | 
				
			||||||
 | 
					  toRaw,
 | 
				
			||||||
 | 
					  toRef,
 | 
				
			||||||
 | 
					  toRefs,
 | 
				
			||||||
 | 
					  toValue,
 | 
				
			||||||
 | 
					  transformVNodeArgs,
 | 
				
			||||||
 | 
					  triggerRef,
 | 
				
			||||||
 | 
					  unref,
 | 
				
			||||||
 | 
					  useAttrs,
 | 
				
			||||||
 | 
					  useCssModule,
 | 
				
			||||||
 | 
					  useCssVars,
 | 
				
			||||||
 | 
					  useHost,
 | 
				
			||||||
 | 
					  useId,
 | 
				
			||||||
 | 
					  useModel,
 | 
				
			||||||
 | 
					  useSSRContext,
 | 
				
			||||||
 | 
					  useShadowRoot,
 | 
				
			||||||
 | 
					  useSlots,
 | 
				
			||||||
 | 
					  useTemplateRef,
 | 
				
			||||||
 | 
					  useTransitionState,
 | 
				
			||||||
 | 
					  vModelCheckbox,
 | 
				
			||||||
 | 
					  vModelDynamic,
 | 
				
			||||||
 | 
					  vModelRadio,
 | 
				
			||||||
 | 
					  vModelSelect,
 | 
				
			||||||
 | 
					  vModelText,
 | 
				
			||||||
 | 
					  vShow,
 | 
				
			||||||
 | 
					  version,
 | 
				
			||||||
 | 
					  warn,
 | 
				
			||||||
 | 
					  watch,
 | 
				
			||||||
 | 
					  watchEffect,
 | 
				
			||||||
 | 
					  watchPostEffect,
 | 
				
			||||||
 | 
					  watchSyncEffect,
 | 
				
			||||||
 | 
					  withAsyncContext,
 | 
				
			||||||
 | 
					  withCtx,
 | 
				
			||||||
 | 
					  withDefaults,
 | 
				
			||||||
 | 
					  withDirectives,
 | 
				
			||||||
 | 
					  withKeys,
 | 
				
			||||||
 | 
					  withMemo,
 | 
				
			||||||
 | 
					  withModifiers,
 | 
				
			||||||
 | 
					  withScopeId
 | 
				
			||||||
 | 
					} from "./chunk-DB3RJHEA.js";
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  BaseTransition,
 | 
				
			||||||
 | 
					  BaseTransitionPropsValidators,
 | 
				
			||||||
 | 
					  Comment,
 | 
				
			||||||
 | 
					  DeprecationTypes,
 | 
				
			||||||
 | 
					  EffectScope,
 | 
				
			||||||
 | 
					  ErrorCodes,
 | 
				
			||||||
 | 
					  ErrorTypeStrings,
 | 
				
			||||||
 | 
					  Fragment,
 | 
				
			||||||
 | 
					  KeepAlive,
 | 
				
			||||||
 | 
					  ReactiveEffect,
 | 
				
			||||||
 | 
					  Static,
 | 
				
			||||||
 | 
					  Suspense,
 | 
				
			||||||
 | 
					  Teleport,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
 | 
					  TrackOpTypes,
 | 
				
			||||||
 | 
					  Transition,
 | 
				
			||||||
 | 
					  TransitionGroup,
 | 
				
			||||||
 | 
					  TriggerOpTypes,
 | 
				
			||||||
 | 
					  VueElement,
 | 
				
			||||||
 | 
					  assertNumber,
 | 
				
			||||||
 | 
					  callWithAsyncErrorHandling,
 | 
				
			||||||
 | 
					  callWithErrorHandling,
 | 
				
			||||||
 | 
					  camelize,
 | 
				
			||||||
 | 
					  capitalize,
 | 
				
			||||||
 | 
					  cloneVNode,
 | 
				
			||||||
 | 
					  compatUtils,
 | 
				
			||||||
 | 
					  compile,
 | 
				
			||||||
 | 
					  computed,
 | 
				
			||||||
 | 
					  createApp,
 | 
				
			||||||
 | 
					  createBlock,
 | 
				
			||||||
 | 
					  createCommentVNode,
 | 
				
			||||||
 | 
					  createElementBlock,
 | 
				
			||||||
 | 
					  createBaseVNode as createElementVNode,
 | 
				
			||||||
 | 
					  createHydrationRenderer,
 | 
				
			||||||
 | 
					  createPropsRestProxy,
 | 
				
			||||||
 | 
					  createRenderer,
 | 
				
			||||||
 | 
					  createSSRApp,
 | 
				
			||||||
 | 
					  createSlots,
 | 
				
			||||||
 | 
					  createStaticVNode,
 | 
				
			||||||
 | 
					  createTextVNode,
 | 
				
			||||||
 | 
					  createVNode,
 | 
				
			||||||
 | 
					  customRef,
 | 
				
			||||||
 | 
					  defineAsyncComponent,
 | 
				
			||||||
 | 
					  defineComponent,
 | 
				
			||||||
 | 
					  defineCustomElement,
 | 
				
			||||||
 | 
					  defineEmits,
 | 
				
			||||||
 | 
					  defineExpose,
 | 
				
			||||||
 | 
					  defineModel,
 | 
				
			||||||
 | 
					  defineOptions,
 | 
				
			||||||
 | 
					  defineProps,
 | 
				
			||||||
 | 
					  defineSSRCustomElement,
 | 
				
			||||||
 | 
					  defineSlots,
 | 
				
			||||||
 | 
					  devtools,
 | 
				
			||||||
 | 
					  effect,
 | 
				
			||||||
 | 
					  effectScope,
 | 
				
			||||||
 | 
					  getCurrentInstance,
 | 
				
			||||||
 | 
					  getCurrentScope,
 | 
				
			||||||
 | 
					  getCurrentWatcher,
 | 
				
			||||||
 | 
					  getTransitionRawChildren,
 | 
				
			||||||
 | 
					  guardReactiveProps,
 | 
				
			||||||
 | 
					  h,
 | 
				
			||||||
 | 
					  handleError,
 | 
				
			||||||
 | 
					  hasInjectionContext,
 | 
				
			||||||
 | 
					  hydrate,
 | 
				
			||||||
 | 
					  hydrateOnIdle,
 | 
				
			||||||
 | 
					  hydrateOnInteraction,
 | 
				
			||||||
 | 
					  hydrateOnMediaQuery,
 | 
				
			||||||
 | 
					  hydrateOnVisible,
 | 
				
			||||||
 | 
					  initCustomFormatter,
 | 
				
			||||||
 | 
					  initDirectivesForSSR,
 | 
				
			||||||
 | 
					  inject,
 | 
				
			||||||
 | 
					  isMemoSame,
 | 
				
			||||||
 | 
					  isProxy,
 | 
				
			||||||
 | 
					  isReactive,
 | 
				
			||||||
 | 
					  isReadonly,
 | 
				
			||||||
 | 
					  isRef,
 | 
				
			||||||
 | 
					  isRuntimeOnly,
 | 
				
			||||||
 | 
					  isShallow,
 | 
				
			||||||
 | 
					  isVNode,
 | 
				
			||||||
 | 
					  markRaw,
 | 
				
			||||||
 | 
					  mergeDefaults,
 | 
				
			||||||
 | 
					  mergeModels,
 | 
				
			||||||
 | 
					  mergeProps,
 | 
				
			||||||
 | 
					  nextTick,
 | 
				
			||||||
 | 
					  normalizeClass,
 | 
				
			||||||
 | 
					  normalizeProps,
 | 
				
			||||||
 | 
					  normalizeStyle,
 | 
				
			||||||
 | 
					  onActivated,
 | 
				
			||||||
 | 
					  onBeforeMount,
 | 
				
			||||||
 | 
					  onBeforeUnmount,
 | 
				
			||||||
 | 
					  onBeforeUpdate,
 | 
				
			||||||
 | 
					  onDeactivated,
 | 
				
			||||||
 | 
					  onErrorCaptured,
 | 
				
			||||||
 | 
					  onMounted,
 | 
				
			||||||
 | 
					  onRenderTracked,
 | 
				
			||||||
 | 
					  onRenderTriggered,
 | 
				
			||||||
 | 
					  onScopeDispose,
 | 
				
			||||||
 | 
					  onServerPrefetch,
 | 
				
			||||||
 | 
					  onUnmounted,
 | 
				
			||||||
 | 
					  onUpdated,
 | 
				
			||||||
 | 
					  onWatcherCleanup,
 | 
				
			||||||
 | 
					  openBlock,
 | 
				
			||||||
 | 
					  popScopeId,
 | 
				
			||||||
 | 
					  provide,
 | 
				
			||||||
 | 
					  proxyRefs,
 | 
				
			||||||
 | 
					  pushScopeId,
 | 
				
			||||||
 | 
					  queuePostFlushCb,
 | 
				
			||||||
 | 
					  reactive,
 | 
				
			||||||
 | 
					  readonly,
 | 
				
			||||||
 | 
					  ref,
 | 
				
			||||||
 | 
					  registerRuntimeCompiler,
 | 
				
			||||||
 | 
					  render,
 | 
				
			||||||
 | 
					  renderList,
 | 
				
			||||||
 | 
					  renderSlot,
 | 
				
			||||||
 | 
					  resolveComponent,
 | 
				
			||||||
 | 
					  resolveDirective,
 | 
				
			||||||
 | 
					  resolveDynamicComponent,
 | 
				
			||||||
 | 
					  resolveFilter,
 | 
				
			||||||
 | 
					  resolveTransitionHooks,
 | 
				
			||||||
 | 
					  setBlockTracking,
 | 
				
			||||||
 | 
					  setDevtoolsHook,
 | 
				
			||||||
 | 
					  setTransitionHooks,
 | 
				
			||||||
 | 
					  shallowReactive,
 | 
				
			||||||
 | 
					  shallowReadonly,
 | 
				
			||||||
 | 
					  shallowRef,
 | 
				
			||||||
 | 
					  ssrContextKey,
 | 
				
			||||||
 | 
					  ssrUtils,
 | 
				
			||||||
 | 
					  stop,
 | 
				
			||||||
 | 
					  toDisplayString,
 | 
				
			||||||
 | 
					  toHandlerKey,
 | 
				
			||||||
 | 
					  toHandlers,
 | 
				
			||||||
 | 
					  toRaw,
 | 
				
			||||||
 | 
					  toRef,
 | 
				
			||||||
 | 
					  toRefs,
 | 
				
			||||||
 | 
					  toValue,
 | 
				
			||||||
 | 
					  transformVNodeArgs,
 | 
				
			||||||
 | 
					  triggerRef,
 | 
				
			||||||
 | 
					  unref,
 | 
				
			||||||
 | 
					  useAttrs,
 | 
				
			||||||
 | 
					  useCssModule,
 | 
				
			||||||
 | 
					  useCssVars,
 | 
				
			||||||
 | 
					  useHost,
 | 
				
			||||||
 | 
					  useId,
 | 
				
			||||||
 | 
					  useModel,
 | 
				
			||||||
 | 
					  useSSRContext,
 | 
				
			||||||
 | 
					  useShadowRoot,
 | 
				
			||||||
 | 
					  useSlots,
 | 
				
			||||||
 | 
					  useTemplateRef,
 | 
				
			||||||
 | 
					  useTransitionState,
 | 
				
			||||||
 | 
					  vModelCheckbox,
 | 
				
			||||||
 | 
					  vModelDynamic,
 | 
				
			||||||
 | 
					  vModelRadio,
 | 
				
			||||||
 | 
					  vModelSelect,
 | 
				
			||||||
 | 
					  vModelText,
 | 
				
			||||||
 | 
					  vShow,
 | 
				
			||||||
 | 
					  version,
 | 
				
			||||||
 | 
					  warn,
 | 
				
			||||||
 | 
					  watch,
 | 
				
			||||||
 | 
					  watchEffect,
 | 
				
			||||||
 | 
					  watchPostEffect,
 | 
				
			||||||
 | 
					  watchSyncEffect,
 | 
				
			||||||
 | 
					  withAsyncContext,
 | 
				
			||||||
 | 
					  withCtx,
 | 
				
			||||||
 | 
					  withDefaults,
 | 
				
			||||||
 | 
					  withDirectives,
 | 
				
			||||||
 | 
					  withKeys,
 | 
				
			||||||
 | 
					  withMemo,
 | 
				
			||||||
 | 
					  withModifiers,
 | 
				
			||||||
 | 
					  withScopeId
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					//# sourceMappingURL=vue.js.map
 | 
				
			||||||
							
								
								
									
										7
									
								
								frontend/node_modules/.vite/deps_temp/vue.js.map
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/node_modules/.vite/deps_temp/vue.js.map
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "version": 3,
 | 
				
			||||||
 | 
					  "sources": [],
 | 
				
			||||||
 | 
					  "sourcesContent": [],
 | 
				
			||||||
 | 
					  "mappings": "",
 | 
				
			||||||
 | 
					  "names": []
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,80 +1,78 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div>
 | 
					  <div class="device-management">
 | 
				
			||||||
    <div class="header">
 | 
					    <header class="header">
 | 
				
			||||||
      <h1>🐷 猪场管理系统</h1>
 | 
					      <h1>设备管理</h1>
 | 
				
			||||||
      <div class="user-info">
 | 
					      <div class="user-info">
 | 
				
			||||||
        <span>{{ username }}</span>
 | 
					        <span>欢迎, {{ username }}</span>
 | 
				
			||||||
        <button class="logout-btn" @click="logout">退出登录</button>
 | 
					        <button class="logout-btn" @click="logout">退出</button>
 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					    </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="nav">
 | 
					    <main class="main-content">
 | 
				
			||||||
      <ul>
 | 
					      <div class="toolbar">
 | 
				
			||||||
        <li><router-link to="/dashboard">控制台</router-link></li>
 | 
					 | 
				
			||||||
        <li><router-link to="/device" class="active">设备管理</router-link></li>
 | 
					 | 
				
			||||||
      </ul>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="container">
 | 
					 | 
				
			||||||
      <div class="page-header">
 | 
					 | 
				
			||||||
        <h2>设备管理</h2>
 | 
					 | 
				
			||||||
        <button class="btn btn-primary" @click="openAddDeviceModal">添加设备</button>
 | 
					        <button class="btn btn-primary" @click="openAddDeviceModal">添加设备</button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div class="device-tree" id="deviceTree">
 | 
					      <div class="device-tree">
 | 
				
			||||||
        <!-- 设备树将通过JavaScript动态生成 -->
 | 
					        <div v-if="devices.length === 0" class="no-devices">
 | 
				
			||||||
        <div v-if="devices.length === 0">
 | 
					 | 
				
			||||||
          暂无设备数据
 | 
					          暂无设备数据
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        
 | 
					        <div v-else>
 | 
				
			||||||
        <div v-for="relay in relayDevices" :key="relay.id" class="tree-node relay-node">
 | 
					          <!-- 中继设备 -->
 | 
				
			||||||
          <div class="node-header">
 | 
					          <div 
 | 
				
			||||||
            <div class="node-title">
 | 
					            v-for="relay in relayDevices" 
 | 
				
			||||||
              <span>{{ relay.name }}</span>
 | 
					            :key="relay.id" 
 | 
				
			||||||
              <span class="node-type relay-type">中继</span>
 | 
					            class="tree-node relay-node"
 | 
				
			||||||
              <span :class="'status-' + relay.status">{{ relay.status === 'active' ? '启用' : '停用' }}</span>
 | 
					          >
 | 
				
			||||||
 | 
					            <div class="node-header" @click="toggleNode(relay.id)">
 | 
				
			||||||
 | 
					              <div class="node-info">
 | 
				
			||||||
 | 
					                <span class="toggle-icon">{{ expandedNodes.has(relay.id) ? '▼' : '►' }}</span>
 | 
				
			||||||
 | 
					                <span class="node-title">{{ relay.name }}</span>
 | 
				
			||||||
 | 
					                <span class="node-type relay-type">{{ getDeviceTypeText(relay.type) }}</span>
 | 
				
			||||||
 | 
					                <span v-if="relay.address" class="node-address">[{{ relay.address }}]</span>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div class="node-actions">
 | 
					              <div class="node-actions">
 | 
				
			||||||
              <button class="action-btn edit-btn" @click="editDevice(relay)">编辑</button>
 | 
					                <button class="action-btn edit-btn" @click.stop="editDevice(relay)">编辑</button>
 | 
				
			||||||
              <button class="action-btn delete-btn" @click="deleteDevice(relay.id)">删除</button>
 | 
					                <button class="action-btn delete-btn" @click.stop="deleteDevice(relay.id)">删除</button>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
          <div class="children" v-if="getControllerDevices(relay.id).length > 0">
 | 
					            <!-- 控制器设备 -->
 | 
				
			||||||
 | 
					            <div v-show="expandedNodes.has(relay.id)" class="children-container">
 | 
				
			||||||
              <div 
 | 
					              <div 
 | 
				
			||||||
                v-for="controller in getControllerDevices(relay.id)" 
 | 
					                v-for="controller in getControllerDevices(relay.id)" 
 | 
				
			||||||
                :key="controller.id" 
 | 
					                :key="controller.id" 
 | 
				
			||||||
                class="tree-node controller-node"
 | 
					                class="tree-node controller-node"
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
              <div class="node-header">
 | 
					                <div class="node-header" @click="toggleNode(controller.id)">
 | 
				
			||||||
                <div class="node-title">
 | 
					                  <div class="node-info">
 | 
				
			||||||
                  <span>{{ controller.name }}</span>
 | 
					                    <span class="toggle-icon">{{ expandedNodes.has(controller.id) ? '▼' : '►' }}</span>
 | 
				
			||||||
                  <span class="node-type controller-type">
 | 
					                    <span class="node-title">{{ controller.name }}</span>
 | 
				
			||||||
                    {{ getDeviceTypeText(controller.type) }}
 | 
					                    <span class="node-type controller-type">{{ getDeviceTypeText(controller.type) }}</span>
 | 
				
			||||||
                  </span>
 | 
					                    <span v-if="controller.address" class="node-address">[{{ controller.address }}]</span>
 | 
				
			||||||
                  <span :class="'status-' + controller.status">{{ controller.status === 'active' ? '启用' : '停用' }}</span>
 | 
					 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <div class="node-actions">
 | 
					                  <div class="node-actions">
 | 
				
			||||||
                  <button class="action-btn edit-btn" @click="editDevice(controller)">编辑</button>
 | 
					                    <button class="action-btn edit-btn" @click.stop="editDevice(controller)">编辑</button>
 | 
				
			||||||
                  <button class="action-btn delete-btn" @click="deleteDevice(controller.id)">删除</button>
 | 
					                    <button class="action-btn delete-btn" @click.stop="deleteDevice(controller.id)">删除</button>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
              <div class="children" v-if="getLeafDevices(controller.id).length > 0">
 | 
					                <!-- 叶子设备 -->
 | 
				
			||||||
 | 
					                <div v-show="expandedNodes.has(controller.id)" class="children-container">
 | 
				
			||||||
                  <div 
 | 
					                  <div 
 | 
				
			||||||
                  v-for="device in getLeafDevices(controller.id)" 
 | 
					                    v-for="leaf in getLeafDevices(controller.id)" 
 | 
				
			||||||
                  :key="device.id" 
 | 
					                    :key="leaf.id" 
 | 
				
			||||||
                    class="tree-node device-node"
 | 
					                    class="tree-node device-node"
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    <div class="node-header">
 | 
					                    <div class="node-header">
 | 
				
			||||||
                    <div class="node-title">
 | 
					                      <div class="node-info">
 | 
				
			||||||
                      <span>{{ device.name }}</span>
 | 
					                        <span class="node-title">{{ leaf.name }}</span>
 | 
				
			||||||
                      <span class="node-type device-type">{{ getDeviceTypeText(device.type) }}</span>
 | 
					                        <span class="node-type device-type">{{ getDeviceTypeText(leaf.type) }}</span>
 | 
				
			||||||
                      <span :class="'status-' + device.status">{{ device.status === 'active' ? '启用' : '停用' }}</span>
 | 
					                        <span v-if="leaf.address" class="node-address">[{{ leaf.address }}]</span>
 | 
				
			||||||
                      </div>
 | 
					                      </div>
 | 
				
			||||||
                      <div class="node-actions">
 | 
					                      <div class="node-actions">
 | 
				
			||||||
                      <button class="action-btn edit-btn" @click="editDevice(device)">编辑</button>
 | 
					                        <button class="action-btn edit-btn" @click.stop="editDevice(leaf)">编辑</button>
 | 
				
			||||||
                      <button class="action-btn delete-btn" @click="deleteDevice(device.id)">删除</button>
 | 
					                        <button class="action-btn delete-btn" @click.stop="deleteDevice(leaf.id)">删除</button>
 | 
				
			||||||
                      </div>
 | 
					                      </div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
@@ -84,6 +82,7 @@
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					    </main>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- 添加/编辑设备模态框 -->
 | 
					    <!-- 添加/编辑设备模态框 -->
 | 
				
			||||||
    <div class="modal" v-if="showModal">
 | 
					    <div class="modal" v-if="showModal">
 | 
				
			||||||
@@ -110,6 +109,23 @@
 | 
				
			|||||||
                <option value="water_curtain">水帘</option>
 | 
					                <option value="water_curtain">水帘</option>
 | 
				
			||||||
              </select>
 | 
					              </select>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <!-- 485总线设备地址字段 -->
 | 
				
			||||||
 | 
					            <div class="form-group" v-if="(deviceForm.type === 'fan' || deviceForm.type === 'water_curtain') && deviceForm.type !== ''">
 | 
				
			||||||
 | 
					              <label for="busNumber">485总线号</label>
 | 
				
			||||||
 | 
					              <input type="number" id="busNumber" v-model.number="deviceForm.bus_number" placeholder="请输入485总线号">
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="form-group" v-if="(deviceForm.type === 'fan' || deviceForm.type === 'water_curtain') && deviceForm.type !== ''">
 | 
				
			||||||
 | 
					              <label for="device485Address">485设备地址</label>
 | 
				
			||||||
 | 
					              <input type="text" id="device485Address" v-model="deviceForm.device_address" placeholder="请输入485设备地址">
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <!-- 非485总线设备地址字段 -->
 | 
				
			||||||
 | 
					            <div class="form-group" v-if="deviceForm.type !== 'relay' && deviceForm.type !== '' && deviceForm.type !== 'fan' && deviceForm.type !== 'water_curtain'">
 | 
				
			||||||
 | 
					              <label for="deviceAddress">设备地址</label>
 | 
				
			||||||
 | 
					              <input type="text" id="deviceAddress" v-model="deviceForm.address" placeholder="请输入设备地址">
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
            <div class="form-group" v-if="deviceForm.type !== 'relay' && deviceForm.type !== ''">
 | 
					            <div class="form-group" v-if="deviceForm.type !== 'relay' && deviceForm.type !== ''">
 | 
				
			||||||
              <label for="parentId">上级设备</label>
 | 
					              <label for="parentId">上级设备</label>
 | 
				
			||||||
              <select id="parentId" v-model="deviceForm.parent_id">
 | 
					              <select id="parentId" v-model="deviceForm.parent_id">
 | 
				
			||||||
@@ -119,7 +135,7 @@
 | 
				
			|||||||
                  :key="parent.id" 
 | 
					                  :key="parent.id" 
 | 
				
			||||||
                  :value="parent.id"
 | 
					                  :value="parent.id"
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                  {{ parent.display_name }}
 | 
					                  {{ parent.name }}
 | 
				
			||||||
                </option>
 | 
					                </option>
 | 
				
			||||||
              </select>
 | 
					              </select>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
@@ -147,8 +163,12 @@ export default {
 | 
				
			|||||||
        id: null,
 | 
					        id: null,
 | 
				
			||||||
        name: '',
 | 
					        name: '',
 | 
				
			||||||
        type: '',
 | 
					        type: '',
 | 
				
			||||||
        parent_id: null
 | 
					        parent_id: null,
 | 
				
			||||||
      }
 | 
					        address: null,
 | 
				
			||||||
 | 
					        bus_number: null,
 | 
				
			||||||
 | 
					        device_address: null
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      expandedNodes: new Set()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
@@ -171,6 +191,15 @@ export default {
 | 
				
			|||||||
      this.$router.push('/')
 | 
					      this.$router.push('/')
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    // 切换节点展开/折叠状态
 | 
				
			||||||
 | 
					    toggleNode(nodeId) {
 | 
				
			||||||
 | 
					      if (this.expandedNodes.has(nodeId)) {
 | 
				
			||||||
 | 
					        this.expandedNodes.delete(nodeId)
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        this.expandedNodes.add(nodeId)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    // 获取控制器设备(区域主控)
 | 
					    // 获取控制器设备(区域主控)
 | 
				
			||||||
    getControllerDevices(parentId) {
 | 
					    getControllerDevices(parentId) {
 | 
				
			||||||
      return this.devices.filter(device => 
 | 
					      return this.devices.filter(device => 
 | 
				
			||||||
@@ -204,26 +233,12 @@ export default {
 | 
				
			|||||||
      if (currentType === 'pig_pen_controller' || currentType === 'feed_mill_controller') {
 | 
					      if (currentType === 'pig_pen_controller' || currentType === 'feed_mill_controller') {
 | 
				
			||||||
        // 控制器的上级是中继设备
 | 
					        // 控制器的上级是中继设备
 | 
				
			||||||
        return this.devices.filter(device => device.type === 'relay')
 | 
					        return this.devices.filter(device => device.type === 'relay')
 | 
				
			||||||
          .map(relay => ({
 | 
					 | 
				
			||||||
            ...relay,
 | 
					 | 
				
			||||||
            display_name: relay.name
 | 
					 | 
				
			||||||
          }));
 | 
					 | 
				
			||||||
      } else if (currentType === 'fan' || currentType === 'water_curtain') {
 | 
					      } else if (currentType === 'fan' || currentType === 'water_curtain') {
 | 
				
			||||||
        // 设备的上级是控制器
 | 
					        // 设备的上级是控制器
 | 
				
			||||||
        // 找到所有控制器设备,并添加其上级中继设备的名称作为前缀
 | 
					        return this.devices.filter(device => 
 | 
				
			||||||
        const controllers = this.devices.filter(device => 
 | 
					          device.type === 'pig_pen_controller' || device.type === 'feed_mill_controller')
 | 
				
			||||||
          device.type === 'pig_pen_controller' || device.type === 'feed_mill_controller');
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        return controllers.map(controller => {
 | 
					 | 
				
			||||||
          // 查找控制器的上级中继设备
 | 
					 | 
				
			||||||
          const relay = this.devices.find(device => device.id === controller.parent_id);
 | 
					 | 
				
			||||||
          return {
 | 
					 | 
				
			||||||
            ...controller,
 | 
					 | 
				
			||||||
            display_name: relay ? `${relay.name} - ${controller.name}` : controller.name
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return [];
 | 
					      return []
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // 加载设备列表
 | 
					    // 加载设备列表
 | 
				
			||||||
@@ -254,7 +269,10 @@ export default {
 | 
				
			|||||||
        id: null,
 | 
					        id: null,
 | 
				
			||||||
        name: '',
 | 
					        name: '',
 | 
				
			||||||
        type: '',
 | 
					        type: '',
 | 
				
			||||||
        parent_id: null
 | 
					        parent_id: null,
 | 
				
			||||||
 | 
					        address: null,
 | 
				
			||||||
 | 
					        bus_number: null,
 | 
				
			||||||
 | 
					        device_address: null
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      this.showModal = true
 | 
					      this.showModal = true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@@ -263,10 +281,32 @@ export default {
 | 
				
			|||||||
    editDevice(device) {
 | 
					    editDevice(device) {
 | 
				
			||||||
      this.editingDevice = device
 | 
					      this.editingDevice = device
 | 
				
			||||||
      this.deviceForm = { ...device }
 | 
					      this.deviceForm = { ...device }
 | 
				
			||||||
      delete this.deviceForm.status
 | 
					      
 | 
				
			||||||
 | 
					      // 如果是485总线设备,尝试解析总线号和设备地址
 | 
				
			||||||
 | 
					      if ((device.type === 'fan' || device.type === 'water_curtain') && device.address) {
 | 
				
			||||||
 | 
					        const parts = device.address.split(':');
 | 
				
			||||||
 | 
					        if (parts.length === 2) {
 | 
				
			||||||
 | 
					          this.deviceForm.bus_number = parseInt(parts[0]);
 | 
				
			||||||
 | 
					          this.deviceForm.device_address = parts[1];
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.deviceForm.device_address = device.address;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
      this.showModal = true
 | 
					      this.showModal = true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 初始化设备表单数据
 | 
				
			||||||
 | 
					    initializeDeviceForm() {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        id: null,
 | 
				
			||||||
 | 
					        name: '',
 | 
				
			||||||
 | 
					        type: '',
 | 
				
			||||||
 | 
					        parent_id: null,
 | 
				
			||||||
 | 
					        address: null
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    // 关闭模态框
 | 
					    // 关闭模态框
 | 
				
			||||||
    closeDeviceModal() {
 | 
					    closeDeviceModal() {
 | 
				
			||||||
      this.showModal = false
 | 
					      this.showModal = false
 | 
				
			||||||
@@ -285,49 +325,48 @@ export default {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        let response
 | 
					        let url, method
 | 
				
			||||||
 | 
					        const deviceData = {
 | 
				
			||||||
 | 
					          name: this.deviceForm.name,
 | 
				
			||||||
 | 
					          type: this.deviceForm.type,
 | 
				
			||||||
 | 
					          parent_id: this.deviceForm.parent_id,
 | 
				
			||||||
 | 
					          address: this.deviceForm.address,
 | 
				
			||||||
 | 
					          bus_number: this.deviceForm.bus_number,
 | 
				
			||||||
 | 
					          device_address: this.deviceForm.device_address
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        if (this.editingDevice) {
 | 
					        if (this.editingDevice) {
 | 
				
			||||||
          // 更新设备
 | 
					          // 更新设备
 | 
				
			||||||
          response = await fetch('/api/v1/device/update', {
 | 
					          url = '/api/v1/device/update'
 | 
				
			||||||
            method: 'POST',
 | 
					          method = 'POST'
 | 
				
			||||||
            headers: {
 | 
					          deviceData.id = this.deviceForm.id
 | 
				
			||||||
              'Content-Type': 'application/json',
 | 
					 | 
				
			||||||
              'Authorization': 'Bearer ' + localStorage.getItem('authToken')
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            body: JSON.stringify({
 | 
					 | 
				
			||||||
              id: this.deviceForm.id,
 | 
					 | 
				
			||||||
              name: this.deviceForm.name,
 | 
					 | 
				
			||||||
              type: this.deviceForm.type,
 | 
					 | 
				
			||||||
              parent_id: this.deviceForm.parent_id
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          // 创建设备
 | 
					          // 创建设备
 | 
				
			||||||
          response = await fetch('/api/v1/device/create', {
 | 
					          url = '/api/v1/device/create'
 | 
				
			||||||
            method: 'POST',
 | 
					          method = 'POST'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const response = await fetch(url, {
 | 
				
			||||||
 | 
					          method: method,
 | 
				
			||||||
          headers: {
 | 
					          headers: {
 | 
				
			||||||
            'Content-Type': 'application/json',
 | 
					            'Content-Type': 'application/json',
 | 
				
			||||||
            'Authorization': 'Bearer ' + localStorage.getItem('authToken')
 | 
					            'Authorization': 'Bearer ' + localStorage.getItem('authToken')
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
            body: JSON.stringify({
 | 
					          body: JSON.stringify(deviceData)
 | 
				
			||||||
              name: this.deviceForm.name,
 | 
					 | 
				
			||||||
              type: this.deviceForm.type,
 | 
					 | 
				
			||||||
              parent_id: this.deviceForm.parent_id
 | 
					 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        const data = await response.json()
 | 
					        const data = await response.json()
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        if (response.ok && data.code === 0) {
 | 
					        if (response.ok && data.code === 0) {
 | 
				
			||||||
 | 
					          // 保存成功,重新加载设备列表
 | 
				
			||||||
 | 
					          await this.loadDevices()
 | 
				
			||||||
          this.closeDeviceModal()
 | 
					          this.closeDeviceModal()
 | 
				
			||||||
          this.loadDevices() // 重新加载设备列表
 | 
					 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          alert('操作失败: ' + data.message)
 | 
					          alert('保存失败: ' + data.message)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error('操作失败:', error)
 | 
					        console.error('保存设备失败:', error)
 | 
				
			||||||
        alert('操作失败,请查看控制台了解详情')
 | 
					        alert('保存设备失败: ' + error.message)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@@ -350,13 +389,30 @@ export default {
 | 
				
			|||||||
        const data = await response.json()
 | 
					        const data = await response.json()
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        if (response.ok && data.code === 0) {
 | 
					        if (response.ok && data.code === 0) {
 | 
				
			||||||
          this.loadDevices() // 重新加载设备列表
 | 
					          // 删除成功,重新加载设备列表
 | 
				
			||||||
 | 
					          await this.loadDevices()
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          alert('删除失败: ' + data.message)
 | 
					          alert('删除失败: ' + data.message)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error('删除失败:', error)
 | 
					        console.error('删除设备失败:', error)
 | 
				
			||||||
        alert('删除失败,请查看控制台了解详情')
 | 
					        alert('删除设备失败: ' + error.message)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 根据设备类型切换上级设备字段显示
 | 
				
			||||||
 | 
					    toggleParentField() {
 | 
				
			||||||
 | 
					      if (this.deviceForm.type === 'relay') {
 | 
				
			||||||
 | 
					        this.deviceForm.parent_id = null
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 切换节点展开/折叠
 | 
				
			||||||
 | 
					    toggleNode(nodeId) {
 | 
				
			||||||
 | 
					      if (this.expandedNodes.has(nodeId)) {
 | 
				
			||||||
 | 
					        this.expandedNodes.delete(nodeId)
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        this.expandedNodes.add(nodeId)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -364,18 +420,26 @@ export default {
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.device-management {
 | 
				
			||||||
 | 
					  height: 100vh;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  background-color: #f5f7fa;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.header {
 | 
					.header {
 | 
				
			||||||
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | 
					  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | 
				
			||||||
  color: white;
 | 
					  color: white;
 | 
				
			||||||
  padding: 20px;
 | 
					  padding: 1rem 2rem;
 | 
				
			||||||
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
 | 
					 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  justify-content: space-between;
 | 
					  justify-content: space-between;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.header h1 {
 | 
					.header h1 {
 | 
				
			||||||
  font-size: 24px;
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  font-size: 1.5rem;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  gap: 10px;
 | 
					  gap: 10px;
 | 
				
			||||||
@@ -384,15 +448,15 @@ export default {
 | 
				
			|||||||
.user-info {
 | 
					.user-info {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  gap: 15px;
 | 
					  gap: 1rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.logout-btn {
 | 
					.logout-btn {
 | 
				
			||||||
  background: rgba(255, 255, 255, 0.2);
 | 
					  background: rgba(255, 255, 255, 0.2);
 | 
				
			||||||
  border: 1px solid rgba(255, 255, 255, 0.3);
 | 
					 | 
				
			||||||
  color: white;
 | 
					  color: white;
 | 
				
			||||||
  padding: 8px 15px;
 | 
					  border: 1px solid rgba(255, 255, 255, 0.3);
 | 
				
			||||||
  border-radius: 5px;
 | 
					  padding: 0.5rem 1rem;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  transition: all 0.3s ease;
 | 
					  transition: all 0.3s ease;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -401,165 +465,188 @@ export default {
 | 
				
			|||||||
  background: rgba(255, 255, 255, 0.3);
 | 
					  background: rgba(255, 255, 255, 0.3);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.nav {
 | 
					.main-content {
 | 
				
			||||||
  background: white;
 | 
					  flex: 1;
 | 
				
			||||||
  padding: 15px 20px;
 | 
					  padding: 1.5rem;
 | 
				
			||||||
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
 | 
					  overflow-y: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.nav ul {
 | 
					.toolbar {
 | 
				
			||||||
 | 
					  margin-bottom: 1.5rem;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  list-style: none;
 | 
					  justify-content: flex-end;
 | 
				
			||||||
  gap: 20px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav a {
 | 
					 | 
				
			||||||
  text-decoration: none;
 | 
					 | 
				
			||||||
  color: #666;
 | 
					 | 
				
			||||||
  padding: 8px 15px;
 | 
					 | 
				
			||||||
  border-radius: 5px;
 | 
					 | 
				
			||||||
  transition: all 0.3s ease;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.nav a:hover, .nav a.active {
 | 
					 | 
				
			||||||
  background: #667eea;
 | 
					 | 
				
			||||||
  color: white;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.container {
 | 
					 | 
				
			||||||
  max-width: 1200px;
 | 
					 | 
				
			||||||
  margin: 30px auto;
 | 
					 | 
				
			||||||
  padding: 0 20px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.page-header {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  justify-content: space-between;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  margin-bottom: 20px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.page-header h2 {
 | 
					 | 
				
			||||||
  color: #333;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.btn {
 | 
					.btn {
 | 
				
			||||||
  padding: 10px 20px;
 | 
					  padding: 0.75rem 1.5rem;
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
  border-radius: 5px;
 | 
					  border-radius: 6px;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 1rem;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
  transition: all 0.3s ease;
 | 
					  transition: all 0.3s ease;
 | 
				
			||||||
 | 
					  display: inline-flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 8px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.btn-primary {
 | 
					.btn-primary {
 | 
				
			||||||
  background: #667eea;
 | 
					  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | 
				
			||||||
  color: white;
 | 
					  color: white;
 | 
				
			||||||
 | 
					  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.btn-primary:hover {
 | 
					.btn-primary:hover {
 | 
				
			||||||
  background: #5a6fd8;
 | 
					  transform: translateY(-2px);
 | 
				
			||||||
 | 
					  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-secondary {
 | 
				
			||||||
 | 
					  background: #f5f5f5;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					  border: 1px solid #ddd;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-secondary:hover {
 | 
				
			||||||
 | 
					  background: #e0e0e0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.device-tree {
 | 
					.device-tree {
 | 
				
			||||||
  background: white;
 | 
					  background: white;
 | 
				
			||||||
  border-radius: 15px;
 | 
					  border-radius: 10px;
 | 
				
			||||||
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
 | 
					  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
  padding: 25px;
 | 
					  padding: 1.5rem;
 | 
				
			||||||
  margin-bottom: 30px;
 | 
					  overflow: hidden;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.tree-node {
 | 
					.tree-node {
 | 
				
			||||||
  margin-bottom: 15px;
 | 
					  margin-bottom: 0.75rem;
 | 
				
			||||||
  border-left: 2px solid #e1e1e1;
 | 
					  border-radius: 8px;
 | 
				
			||||||
  padding-left: 20px;
 | 
					  transition: all 0.3s ease;
 | 
				
			||||||
 | 
					  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.relay-node {
 | 
					.tree-node:hover {
 | 
				
			||||||
  border-left-color: #667eea;
 | 
					  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.controller-node {
 | 
					.tree-node.relay-node {
 | 
				
			||||||
  border-left-color: #764ba2;
 | 
					  background: #fff3e0;
 | 
				
			||||||
 | 
					  border-left: 4px solid #ff9800;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.device-node {
 | 
					.tree-node.controller-node {
 | 
				
			||||||
  border-left-color: #ffa500;
 | 
					  background: #e3f2fd;
 | 
				
			||||||
 | 
					  border-left: 4px solid #2196f3;
 | 
				
			||||||
 | 
					  margin-left: 1.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tree-node.device-node {
 | 
				
			||||||
 | 
					  background: #e8f5e9;
 | 
				
			||||||
 | 
					  border-left: 4px solid #4caf50;
 | 
				
			||||||
 | 
					  margin-left: 3rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.node-header {
 | 
					.node-header {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  justify-content: space-between;
 | 
					  justify-content: space-between;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  padding: 10px;
 | 
					  padding: 1rem;
 | 
				
			||||||
  background: #f8f9fa;
 | 
					  cursor: pointer;
 | 
				
			||||||
  border-radius: 5px;
 | 
					 | 
				
			||||||
  margin-bottom: 10px;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.relay-node > .node-header {
 | 
					.node-header:hover {
 | 
				
			||||||
  background: rgba(102, 126, 234, 0.1);
 | 
					  background-color: rgba(0, 0, 0, 0.02);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.controller-node > .node-header {
 | 
					.node-info {
 | 
				
			||||||
  background: rgba(118, 75, 162, 0.1);
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 0.75rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.device-node > .node-header {
 | 
					.toggle-icon {
 | 
				
			||||||
  background: rgba(255, 165, 0, 0.1);
 | 
					  width: 20px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  font-size: 0.8rem;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.node-title {
 | 
					.node-title {
 | 
				
			||||||
  font-weight: bold;
 | 
					  font-weight: 600;
 | 
				
			||||||
  display: flex;
 | 
					  color: #333;
 | 
				
			||||||
  align-items: center;
 | 
					  font-size: 1.1rem;
 | 
				
			||||||
  gap: 10px;
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.node-address {
 | 
				
			||||||
 | 
					  font-size: 0.9rem;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					  background: rgba(0, 0, 0, 0.05);
 | 
				
			||||||
 | 
					  padding: 0.2rem 0.5rem;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.node-type {
 | 
					.node-type {
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 0.8rem;
 | 
				
			||||||
  padding: 2px 8px;
 | 
					  padding: 0.3rem 0.6rem;
 | 
				
			||||||
  border-radius: 10px;
 | 
					  border-radius: 12px;
 | 
				
			||||||
  color: white;
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					  text-transform: uppercase;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.relay-type {
 | 
					.relay-type {
 | 
				
			||||||
  background: #667eea;
 | 
					  background: #ff9800;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.controller-type {
 | 
					.controller-type {
 | 
				
			||||||
  background: #764ba2;
 | 
					  background: #2196f3;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.device-type {
 | 
					.device-type {
 | 
				
			||||||
  background: #ffa500;
 | 
					  background: #4caf50;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.node-actions {
 | 
					.node-actions {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  gap: 10px;
 | 
					  gap: 0.5rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.action-btn {
 | 
					.action-btn {
 | 
				
			||||||
  padding: 5px 10px;
 | 
					  padding: 0.4rem 0.8rem;
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
  border-radius: 3px;
 | 
					  border-radius: 4px;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 0.85rem;
 | 
				
			||||||
 | 
					  transition: all 0.2s ease;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.edit-btn {
 | 
					.edit-btn {
 | 
				
			||||||
  background: #ffc107;
 | 
					  background: #1976d2;
 | 
				
			||||||
  color: #333;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.delete-btn {
 | 
					 | 
				
			||||||
  background: #dc3545;
 | 
					 | 
				
			||||||
  color: white;
 | 
					  color: white;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.children {
 | 
					.edit-btn:hover {
 | 
				
			||||||
  margin-left: 20px;
 | 
					  background: #1565c0;
 | 
				
			||||||
 | 
					  transform: translateY(-1px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.delete-btn {
 | 
				
			||||||
 | 
					  background: #d32f2f;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.delete-btn:hover {
 | 
				
			||||||
 | 
					  background: #c62828;
 | 
				
			||||||
 | 
					  transform: translateY(-1px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.children-container {
 | 
				
			||||||
 | 
					  padding: 0.5rem 0 0.5rem 1rem;
 | 
				
			||||||
 | 
					  border-left: 2px dashed rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					  margin-left: 1rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.modal {
 | 
					.modal {
 | 
				
			||||||
@@ -569,100 +656,142 @@ export default {
 | 
				
			|||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  height: 100%;
 | 
					  height: 100%;
 | 
				
			||||||
  background: rgba(0, 0, 0, 0.5);
 | 
					  background: rgba(0, 0, 0, 0.5);
 | 
				
			||||||
  z-index: 1000;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  z-index: 1000;
 | 
				
			||||||
 | 
					  animation: fadeIn 0.3s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes fadeIn {
 | 
				
			||||||
 | 
					  from { opacity: 0; }
 | 
				
			||||||
 | 
					  to { opacity: 1; }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.modal-content {
 | 
					.modal-content {
 | 
				
			||||||
  background: white;
 | 
					  background: white;
 | 
				
			||||||
  border-radius: 15px;
 | 
					  border-radius: 10px;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 90%;
 | 
				
			||||||
  max-width: 500px;
 | 
					  max-width: 500px;
 | 
				
			||||||
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
 | 
					  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
 | 
				
			||||||
 | 
					  animation: slideIn 0.3s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes slideIn {
 | 
				
			||||||
 | 
					  from {
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					    transform: translateY(-50px);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  to {
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					    transform: translateY(0);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.modal-header {
 | 
					.modal-header {
 | 
				
			||||||
  padding: 20px;
 | 
					 | 
				
			||||||
  border-bottom: 1px solid #eee;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  justify-content: space-between;
 | 
					  justify-content: space-between;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  padding: 1.5rem;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #eee;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.modal-header h3 {
 | 
					.modal-header h3 {
 | 
				
			||||||
  margin: 0;
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					  font-size: 1.3rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.close-btn {
 | 
					.close-btn {
 | 
				
			||||||
  background: none;
 | 
					  background: none;
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
  font-size: 24px;
 | 
					  font-size: 1.8rem;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  width: 30px;
 | 
				
			||||||
 | 
					  height: 30px;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
  color: #999;
 | 
					  color: #999;
 | 
				
			||||||
 | 
					  transition: color 0.2s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.close-btn:hover {
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.modal-body {
 | 
					.modal-body {
 | 
				
			||||||
  padding: 20px;
 | 
					  padding: 1.5rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.form-group {
 | 
					.form-group {
 | 
				
			||||||
  margin-bottom: 20px;
 | 
					  margin-bottom: 1.5rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.form-group label {
 | 
					.form-group label {
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  margin-bottom: 5px;
 | 
					  margin-bottom: 0.5rem;
 | 
				
			||||||
  font-weight: 500;
 | 
					  font-weight: 500;
 | 
				
			||||||
  color: #333;
 | 
					  color: #333;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.form-group input, .form-group select {
 | 
					.form-group input,
 | 
				
			||||||
 | 
					.form-group select {
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  padding: 10px;
 | 
					  padding: 0.8rem;
 | 
				
			||||||
  border: 1px solid #ddd;
 | 
					  border: 1px solid #ddd;
 | 
				
			||||||
  border-radius: 5px;
 | 
					  border-radius: 6px;
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 1rem;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  transition: border-color 0.2s ease, box-shadow 0.2s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-group input:focus,
 | 
				
			||||||
 | 
					.form-group select:focus {
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					  border-color: #667eea;
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.modal-footer {
 | 
					.modal-footer {
 | 
				
			||||||
  padding: 20px;
 | 
					  padding: 1.5rem;
 | 
				
			||||||
  border-top: 1px solid #eee;
 | 
					  border-top: 1px solid #eee;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  justify-content: flex-end;
 | 
					  justify-content: flex-end;
 | 
				
			||||||
  gap: 10px;
 | 
					  gap: 0.75rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.btn-secondary {
 | 
					.no-devices {
 | 
				
			||||||
  background: #6c757d;
 | 
					  text-align: center;
 | 
				
			||||||
  color: white;
 | 
					  color: #7f8c8d;
 | 
				
			||||||
}
 | 
					  padding: 3rem;
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
.status-active {
 | 
					  font-size: 1.1rem;
 | 
				
			||||||
  color: #28a745;
 | 
					 | 
				
			||||||
  font-weight: bold;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status-inactive {
 | 
					 | 
				
			||||||
  color: #dc3545;
 | 
					 | 
				
			||||||
  font-weight: bold;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media (max-width: 768px) {
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
  .dashboard-grid {
 | 
					  .main-content {
 | 
				
			||||||
    grid-template-columns: 1fr;
 | 
					    padding: 1rem;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  .header {
 | 
					  .header {
 | 
				
			||||||
    flex-direction: column;
 | 
					    padding: 1rem;
 | 
				
			||||||
    gap: 15px;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  .page-header {
 | 
					  .node-header {
 | 
				
			||||||
    flex-direction: column;
 | 
					    flex-direction: column;
 | 
				
			||||||
    gap: 15px;
 | 
					 | 
				
			||||||
    align-items: flex-start;
 | 
					    align-items: flex-start;
 | 
				
			||||||
 | 
					    gap: 0.75rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .node-actions {
 | 
				
			||||||
 | 
					    align-self: flex-end;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .tree-node.controller-node,
 | 
				
			||||||
 | 
					  .tree-node.device-node {
 | 
				
			||||||
 | 
					    margin-left: 1rem;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
@@ -25,6 +25,11 @@ type DeviceRequest struct {
 | 
				
			|||||||
	Name     string           `json:"name" binding:"required"` // 设备名称,必填
 | 
						Name     string           `json:"name" binding:"required"` // 设备名称,必填
 | 
				
			||||||
	Type     model.DeviceType `json:"type" binding:"required"` // 设备类型,必填
 | 
						Type     model.DeviceType `json:"type" binding:"required"` // 设备类型,必填
 | 
				
			||||||
	ParentID *uint            `json:"parent_id,omitempty"`     // 父设备ID,可选
 | 
						ParentID *uint            `json:"parent_id,omitempty"`     // 父设备ID,可选
 | 
				
			||||||
 | 
						Address  *string          `json:"address,omitempty"`       // 设备地址,可选
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 485总线设备的额外字段
 | 
				
			||||||
 | 
						BusNumber     *int    `json:"bus_number,omitempty"`     // 485总线号
 | 
				
			||||||
 | 
						DeviceAddress *string `json:"device_address,omitempty"` // 485设备地址
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// BindAndValidate 绑定并验证请求数据
 | 
					// BindAndValidate 绑定并验证请求数据
 | 
				
			||||||
@@ -64,6 +69,45 @@ func (req *DeviceRequest) BindAndValidate(data []byte) error {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 特殊处理address字段
 | 
				
			||||||
 | 
						if addressVal, exists := raw["address"]; exists && addressVal != nil {
 | 
				
			||||||
 | 
							switch v := addressVal.(type) {
 | 
				
			||||||
 | 
							case string:
 | 
				
			||||||
 | 
								// 如果是字符串,直接赋值
 | 
				
			||||||
 | 
								if v != "" {
 | 
				
			||||||
 | 
									req.Address = &v
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 特殊处理bus_number字段
 | 
				
			||||||
 | 
						if busNumberVal, exists := raw["bus_number"]; exists && busNumberVal != nil {
 | 
				
			||||||
 | 
							switch v := busNumberVal.(type) {
 | 
				
			||||||
 | 
							case float64:
 | 
				
			||||||
 | 
								// JSON数字默认是float64类型
 | 
				
			||||||
 | 
								busNumber := int(v)
 | 
				
			||||||
 | 
								req.BusNumber = &busNumber
 | 
				
			||||||
 | 
							case string:
 | 
				
			||||||
 | 
								// 如果是字符串,尝试转换为int
 | 
				
			||||||
 | 
								if v != "" && v != "null" {
 | 
				
			||||||
 | 
									if busNumber, err := strconv.Atoi(v); err == nil {
 | 
				
			||||||
 | 
										req.BusNumber = &busNumber
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 特殊处理device_address字段
 | 
				
			||||||
 | 
						if deviceAddressVal, exists := raw["device_address"]; exists && deviceAddressVal != nil {
 | 
				
			||||||
 | 
							switch v := deviceAddressVal.(type) {
 | 
				
			||||||
 | 
							case string:
 | 
				
			||||||
 | 
								// 如果是字符串,直接赋值
 | 
				
			||||||
 | 
								if v != "" {
 | 
				
			||||||
 | 
									req.DeviceAddress = &v
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -113,13 +157,17 @@ func (c *Controller) Create(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// TODO: 设备状态应该由系统自动获取,而不是由用户指定
 | 
					 | 
				
			||||||
	// 这里设置默认状态为active,后续需要实现自动状态检测
 | 
					 | 
				
			||||||
	device := &model.Device{
 | 
						device := &model.Device{
 | 
				
			||||||
		Name:     req.Name,
 | 
							Name:     req.Name,
 | 
				
			||||||
		Type:     req.Type,
 | 
							Type:     req.Type,
 | 
				
			||||||
		ParentID: req.ParentID,
 | 
							ParentID: req.ParentID,
 | 
				
			||||||
		Status:   "active", // 默认设置为active状态
 | 
							Address:  req.Address,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 如果是485总线设备且提供了总线号和设备地址,则合并为一个地址
 | 
				
			||||||
 | 
						if (req.Type == model.DeviceTypeFan || req.Type == model.DeviceTypeWaterCurtain) &&
 | 
				
			||||||
 | 
							req.BusNumber != nil && req.DeviceAddress != nil {
 | 
				
			||||||
 | 
							device.Set485Address(*req.BusNumber, *req.DeviceAddress)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := c.deviceRepo.Create(device); err != nil {
 | 
						if err := c.deviceRepo.Create(device); err != nil {
 | 
				
			||||||
@@ -176,9 +224,15 @@ func (c *Controller) Update(ctx *gin.Context) {
 | 
				
			|||||||
	device.Name = req.Name
 | 
						device.Name = req.Name
 | 
				
			||||||
	device.Type = req.Type
 | 
						device.Type = req.Type
 | 
				
			||||||
	device.ParentID = req.ParentID
 | 
						device.ParentID = req.ParentID
 | 
				
			||||||
 | 
						device.Address = req.Address
 | 
				
			||||||
 | 
						// 如果是485总线设备且提供了总线号和设备地址,则合并为一个地址
 | 
				
			||||||
 | 
						if (req.Type == model.DeviceTypeFan || req.Type == model.DeviceTypeWaterCurtain) &&
 | 
				
			||||||
 | 
							req.BusNumber != nil && req.DeviceAddress != nil {
 | 
				
			||||||
 | 
							device.Set485Address(*req.BusNumber, *req.DeviceAddress)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	// TODO: 设备状态应该由系统自动获取,而不是由用户指定
 | 
						// TODO: 设备状态应该由系统自动获取,而不是由用户指定
 | 
				
			||||||
	// 这里保持设备原有状态,后续需要实现自动状态检测
 | 
						// 这里保持设备原有状态,后续需要实现自动状态检测
 | 
				
			||||||
	// device.Status = req.Status
 | 
						// 设备状态现在只在内存中维护,不持久化到数据库
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := c.deviceRepo.Update(device); err != nil {
 | 
						if err := c.deviceRepo.Update(device); err != nil {
 | 
				
			||||||
		c.logger.Error("更新设备失败: " + err.Error())
 | 
							c.logger.Error("更新设备失败: " + err.Error())
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,9 @@
 | 
				
			|||||||
package model
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"gorm.io/gorm"
 | 
						"gorm.io/gorm"
 | 
				
			||||||
@@ -42,8 +45,10 @@ type Device struct {
 | 
				
			|||||||
	// ParentID 上级设备ID(用于设备层级关系,指向区域主控设备)
 | 
						// ParentID 上级设备ID(用于设备层级关系,指向区域主控设备)
 | 
				
			||||||
	ParentID *uint `gorm:"column:parent_id;index" json:"parent_id"`
 | 
						ParentID *uint `gorm:"column:parent_id;index" json:"parent_id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Status 设备状态
 | 
						// Address 设备地址(普通设备的485总线地址或区域主控的Lora地址,中继设备不需要)
 | 
				
			||||||
	Status string `gorm:"not null;column:status" json:"status"`
 | 
						// 格式:对于普通设备,可以是"bus_number:device_address"或"device_address"
 | 
				
			||||||
 | 
						//       对于区域主控,是Lora地址
 | 
				
			||||||
 | 
						Address *string `gorm:"column:address" json:"address,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// CreatedAt 创建时间
 | 
						// CreatedAt 创建时间
 | 
				
			||||||
	CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
 | 
						CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
 | 
				
			||||||
@@ -60,6 +65,37 @@ func (Device) TableName() string {
 | 
				
			|||||||
	return "devices"
 | 
						return "devices"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Set485Address 设置普通设备的485总线地址和设备地址
 | 
				
			||||||
 | 
					func (d *Device) Set485Address(busNumber int, deviceAddress string) {
 | 
				
			||||||
 | 
						if d.Type != DeviceTypeFan && d.Type != DeviceTypeWaterCurtain {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						address := fmt.Sprintf("%d:%s", busNumber, deviceAddress)
 | 
				
			||||||
 | 
						d.Address = &address
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Get485Address 获取普通设备的总线号和设备地址
 | 
				
			||||||
 | 
					func (d *Device) Get485Address() (busNumber int, deviceAddress string, err error) {
 | 
				
			||||||
 | 
						if d.Address == nil {
 | 
				
			||||||
 | 
							return 0, "", fmt.Errorf("address is nil")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						parts := strings.Split(*d.Address, ":")
 | 
				
			||||||
 | 
						if len(parts) != 2 {
 | 
				
			||||||
 | 
							// 如果没有总线号,默认为总线0
 | 
				
			||||||
 | 
							return 0, *d.Address, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						busNumber, err = strconv.Atoi(parts[0])
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, "", fmt.Errorf("invalid bus number: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						deviceAddress = parts[1]
 | 
				
			||||||
 | 
						return busNumber, deviceAddress, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// DeviceControl 代表设备控制记录
 | 
					// DeviceControl 代表设备控制记录
 | 
				
			||||||
type DeviceControl struct {
 | 
					type DeviceControl struct {
 | 
				
			||||||
	// ID 记录ID
 | 
						// ID 记录ID
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user