Compare commits
	
		
			3 Commits
		
	
	
		
			fead740a0f
			...
			e5383ce268
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e5383ce268 | |||
| 0b16d7d886 | |||
| ca3cf02396 | 
							
								
								
									
										336
									
								
								frontend/dashboard.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								frontend/dashboard.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,336 @@
 | 
			
		||||
<!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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .device-control h2 {
 | 
			
		||||
            color: #333;
 | 
			
		||||
            margin-bottom: 20px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .form-group {
 | 
			
		||||
            margin-bottom: 20px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        label {
 | 
			
		||||
            display: block;
 | 
			
		||||
            margin-bottom: 8px;
 | 
			
		||||
            color: #555;
 | 
			
		||||
            font-weight: 500;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        select, input {
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            padding: 12px;
 | 
			
		||||
            border: 2px solid #e1e1e1;
 | 
			
		||||
            border-radius: 8px;
 | 
			
		||||
            font-size: 16px;
 | 
			
		||||
            outline: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        select:focus, input:focus {
 | 
			
		||||
            border-color: #667eea;
 | 
			
		||||
            box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        button {
 | 
			
		||||
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | 
			
		||||
            color: white;
 | 
			
		||||
            border: none;
 | 
			
		||||
            padding: 12px 25px;
 | 
			
		||||
            border-radius: 8px;
 | 
			
		||||
            font-size: 16px;
 | 
			
		||||
            font-weight: 600;
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
            transition: all 0.3s ease;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        button:hover {
 | 
			
		||||
            transform: translateY(-2px);
 | 
			
		||||
            box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .status-indicator {
 | 
			
		||||
            display: inline-block;
 | 
			
		||||
            width: 12px;
 | 
			
		||||
            height: 12px;
 | 
			
		||||
            border-radius: 50%;
 | 
			
		||||
            margin-right: 8px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .status-online {
 | 
			
		||||
            background-color: #27ae60;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .status-offline {
 | 
			
		||||
            background-color: #e74c3c;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .notification {
 | 
			
		||||
            padding: 15px;
 | 
			
		||||
            border-radius: 8px;
 | 
			
		||||
            margin: 15px 0;
 | 
			
		||||
            display: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .notification.success {
 | 
			
		||||
            background-color: #d4edda;
 | 
			
		||||
            color: #155724;
 | 
			
		||||
            border: 1px solid #c3e6cb;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .notification.error {
 | 
			
		||||
            background-color: #f8d7da;
 | 
			
		||||
            color: #721c24;
 | 
			
		||||
            border: 1px solid #f5c6cb;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @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 class="notification" id="controlNotification"></div>
 | 
			
		||||
            
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                <label for="deviceType">设备类型</label>
 | 
			
		||||
                <select id="deviceType">
 | 
			
		||||
                    <option value="fan">风扇</option>
 | 
			
		||||
                    <option value="water_curtain">水帘</option>
 | 
			
		||||
                </select>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                <label for="deviceId">设备ID</label>
 | 
			
		||||
                <input type="text" id="deviceId" placeholder="请输入设备ID">
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                <label for="action">操作</label>
 | 
			
		||||
                <select id="action">
 | 
			
		||||
                    <option value="on">开启</option>
 | 
			
		||||
                    <option value="off">关闭</option>
 | 
			
		||||
                </select>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <button id="controlBtn">执行控制</button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <script>
 | 
			
		||||
        // 检查用户是否已登录
 | 
			
		||||
        const token = localStorage.getItem('authToken');
 | 
			
		||||
        const username = localStorage.getItem('username');
 | 
			
		||||
        
 | 
			
		||||
        if (!token) {
 | 
			
		||||
            window.location.href = '/index.html';
 | 
			
		||||
        } else {
 | 
			
		||||
            document.getElementById('username').textContent = username;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 退出登录
 | 
			
		||||
        document.getElementById('logoutBtn').addEventListener('click', function() {
 | 
			
		||||
            localStorage.removeItem('authToken');
 | 
			
		||||
            localStorage.removeItem('userId');
 | 
			
		||||
            localStorage.removeItem('username');
 | 
			
		||||
            window.location.href = '/index.html';
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 设备控制
 | 
			
		||||
        document.getElementById('controlBtn').addEventListener('click', async function() {
 | 
			
		||||
            const deviceType = document.getElementById('deviceType').value;
 | 
			
		||||
            const deviceId = document.getElementById('deviceId').value;
 | 
			
		||||
            const action = document.getElementById('action').value;
 | 
			
		||||
            const notification = document.getElementById('controlNotification');
 | 
			
		||||
            
 | 
			
		||||
            if (!deviceId) {
 | 
			
		||||
                showNotification('请输入设备ID', 'error');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await fetch('/api/v1/device/switch', {
 | 
			
		||||
                    method: 'POST',
 | 
			
		||||
                    headers: {
 | 
			
		||||
                        'Content-Type': 'application/json',
 | 
			
		||||
                        'Authorization': `Bearer ${token}`
 | 
			
		||||
                    },
 | 
			
		||||
                    body: JSON.stringify({ 
 | 
			
		||||
                        device_type: deviceType,
 | 
			
		||||
                        device_id: deviceId,
 | 
			
		||||
                        action: action
 | 
			
		||||
                    })
 | 
			
		||||
                });
 | 
			
		||||
                
 | 
			
		||||
                const data = await response.json();
 | 
			
		||||
                
 | 
			
		||||
                if (response.ok && data.code === 0) {
 | 
			
		||||
                    showNotification(`设备控制成功: ${data.message}`, 'success');
 | 
			
		||||
                } else {
 | 
			
		||||
                    showNotification(`控制失败: ${data.message || '未知错误'}`, 'error');
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                showNotification('网络错误,请稍后重试', 'error');
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        // 显示通知
 | 
			
		||||
        function showNotification(message, type) {
 | 
			
		||||
            const notification = document.getElementById('controlNotification');
 | 
			
		||||
            notification.textContent = message;
 | 
			
		||||
            notification.className = 'notification ' + type;
 | 
			
		||||
            notification.style.display = 'block';
 | 
			
		||||
            
 | 
			
		||||
            // 3秒后自动隐藏
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                notification.style.display = 'none';
 | 
			
		||||
            }, 3000);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // 模拟获取设备数量
 | 
			
		||||
        document.getElementById('deviceCount').textContent = '12';
 | 
			
		||||
        document.getElementById('operationCount').textContent = '24';
 | 
			
		||||
    </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										272
									
								
								frontend/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										272
									
								
								frontend/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,272 @@
 | 
			
		||||
<!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: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | 
			
		||||
            min-height: 100vh;
 | 
			
		||||
            display: flex;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .login-container {
 | 
			
		||||
            background: rgba(255, 255, 255, 0.95);
 | 
			
		||||
            backdrop-filter: blur(10px);
 | 
			
		||||
            border-radius: 20px;
 | 
			
		||||
            box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
 | 
			
		||||
            padding: 40px;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            max-width: 400px;
 | 
			
		||||
            text-align: center;
 | 
			
		||||
            animation: fadeIn 0.5s ease-out;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes fadeIn {
 | 
			
		||||
            from {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-20px);
 | 
			
		||||
            }
 | 
			
		||||
            to {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .logo {
 | 
			
		||||
            width: 80px;
 | 
			
		||||
            height: 80px;
 | 
			
		||||
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | 
			
		||||
            border-radius: 50%;
 | 
			
		||||
            margin: 0 auto 20px;
 | 
			
		||||
            display: flex;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
            color: white;
 | 
			
		||||
            font-size: 30px;
 | 
			
		||||
            font-weight: bold;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        h1 {
 | 
			
		||||
            color: #333;
 | 
			
		||||
            margin-bottom: 10px;
 | 
			
		||||
            font-size: 28px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .subtitle {
 | 
			
		||||
            color: #666;
 | 
			
		||||
            margin-bottom: 30px;
 | 
			
		||||
            font-size: 14px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .form-group {
 | 
			
		||||
            margin-bottom: 20px;
 | 
			
		||||
            text-align: left;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        label {
 | 
			
		||||
            display: block;
 | 
			
		||||
            margin-bottom: 8px;
 | 
			
		||||
            color: #555;
 | 
			
		||||
            font-weight: 500;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        input {
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            padding: 15px;
 | 
			
		||||
            border: 2px solid #e1e1e1;
 | 
			
		||||
            border-radius: 10px;
 | 
			
		||||
            font-size: 16px;
 | 
			
		||||
            transition: all 0.3s ease;
 | 
			
		||||
            outline: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        input:focus {
 | 
			
		||||
            border-color: #667eea;
 | 
			
		||||
            box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        button {
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            padding: 15px;
 | 
			
		||||
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | 
			
		||||
            color: white;
 | 
			
		||||
            border: none;
 | 
			
		||||
            border-radius: 10px;
 | 
			
		||||
            font-size: 16px;
 | 
			
		||||
            font-weight: 600;
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
            transition: all 0.3s ease;
 | 
			
		||||
            margin-top: 10px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        button:hover {
 | 
			
		||||
            transform: translateY(-2px);
 | 
			
		||||
            box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        button:active {
 | 
			
		||||
            transform: translateY(0);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        button:disabled {
 | 
			
		||||
            opacity: 0.7;
 | 
			
		||||
            cursor: not-allowed;
 | 
			
		||||
            transform: none;
 | 
			
		||||
            box-shadow: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .error-message {
 | 
			
		||||
            color: #e74c3c;
 | 
			
		||||
            margin-top: 15px;
 | 
			
		||||
            padding: 10px;
 | 
			
		||||
            border-radius: 5px;
 | 
			
		||||
            background-color: #fdf2f2;
 | 
			
		||||
            border: 1px solid #f5c6cb;
 | 
			
		||||
            display: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .success-message {
 | 
			
		||||
            color: #27ae60;
 | 
			
		||||
            margin-top: 15px;
 | 
			
		||||
            padding: 10px;
 | 
			
		||||
            border-radius: 5px;
 | 
			
		||||
            background-color: #f2fdf2;
 | 
			
		||||
            border: 1px solid #c6f5cb;
 | 
			
		||||
            display: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .loading {
 | 
			
		||||
            display: none;
 | 
			
		||||
            margin: 20px auto;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .loading-spinner {
 | 
			
		||||
            border: 3px solid #f3f3f3;
 | 
			
		||||
            border-top: 3px solid #667eea;
 | 
			
		||||
            border-radius: 50%;
 | 
			
		||||
            width: 30px;
 | 
			
		||||
            height: 30px;
 | 
			
		||||
            animation: spin 1s linear infinite;
 | 
			
		||||
            margin: 0 auto;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes spin {
 | 
			
		||||
            0% { transform: rotate(0deg); }
 | 
			
		||||
            100% { transform: rotate(360deg); }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @media (max-width: 480px) {
 | 
			
		||||
            .login-container {
 | 
			
		||||
                margin: 20px;
 | 
			
		||||
                padding: 30px 20px;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            h1 {
 | 
			
		||||
                font-size: 24px;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div class="login-container">
 | 
			
		||||
        <div class="logo">🐷</div>
 | 
			
		||||
        <h1>猪场管理系统</h1>
 | 
			
		||||
        <p class="subtitle">请登录您的账户</p>
 | 
			
		||||
        
 | 
			
		||||
        <form id="loginForm">
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                <label for="username">用户名</label>
 | 
			
		||||
                <input type="text" id="username" name="username" required placeholder="请输入用户名">
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                <label for="password">密码</label>
 | 
			
		||||
                <input type="password" id="password" name="password" required placeholder="请输入密码">
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
            <button type="submit" id="loginBtn">登录</button>
 | 
			
		||||
        </form>
 | 
			
		||||
        
 | 
			
		||||
        <div class="loading" id="loading">
 | 
			
		||||
            <div class="loading-spinner"></div>
 | 
			
		||||
            <p>登录中...</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div class="error-message" id="errorMessage"></div>
 | 
			
		||||
        <div class="success-message" id="successMessage"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <script>
 | 
			
		||||
        document.getElementById('loginForm').addEventListener('submit', async function(e) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            
 | 
			
		||||
            const username = document.getElementById('username').value;
 | 
			
		||||
            const password = document.getElementById('password').value;
 | 
			
		||||
            const loginBtn = document.getElementById('loginBtn');
 | 
			
		||||
            const loading = document.getElementById('loading');
 | 
			
		||||
            const errorMessage = document.getElementById('errorMessage');
 | 
			
		||||
            const successMessage = document.getElementById('successMessage');
 | 
			
		||||
            
 | 
			
		||||
            // 隐藏之前的消息
 | 
			
		||||
            errorMessage.style.display = 'none';
 | 
			
		||||
            successMessage.style.display = 'none';
 | 
			
		||||
            
 | 
			
		||||
            // 显示加载状态
 | 
			
		||||
            loginBtn.disabled = true;
 | 
			
		||||
            loading.style.display = 'block';
 | 
			
		||||
            
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await fetch('/api/v1/user/login', {
 | 
			
		||||
                    method: 'POST',
 | 
			
		||||
                    headers: {
 | 
			
		||||
                        'Content-Type': 'application/json'
 | 
			
		||||
                    },
 | 
			
		||||
                    body: JSON.stringify({ username, password })
 | 
			
		||||
                });
 | 
			
		||||
                
 | 
			
		||||
                const data = await response.json();
 | 
			
		||||
                
 | 
			
		||||
                if (response.ok && data.code === 0) {
 | 
			
		||||
                    // 登录成功
 | 
			
		||||
                    successMessage.textContent = data.message;
 | 
			
		||||
                    successMessage.style.display = 'block';
 | 
			
		||||
                    
 | 
			
		||||
                    // 保存token到localStorage
 | 
			
		||||
                    localStorage.setItem('authToken', data.data.token);
 | 
			
		||||
                    localStorage.setItem('userId', data.data.id);
 | 
			
		||||
                    localStorage.setItem('username', data.data.username);
 | 
			
		||||
                    
 | 
			
		||||
                    // 跳转到主页面(这里暂时只是显示成功信息)
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        alert('登录成功!将跳转到主页面。');
 | 
			
		||||
                        // 这里可以重定向到主页面
 | 
			
		||||
                        // window.location.href = '/dashboard.html';
 | 
			
		||||
                    }, 1000);
 | 
			
		||||
                } else {
 | 
			
		||||
                    // 登录失败
 | 
			
		||||
                    errorMessage.textContent = data.message || '登录失败';
 | 
			
		||||
                    errorMessage.style.display = 'block';
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                errorMessage.textContent = '网络错误,请稍后重试';
 | 
			
		||||
                errorMessage.style.display = 'block';
 | 
			
		||||
            } finally {
 | 
			
		||||
                // 恢复按钮状态
 | 
			
		||||
                loginBtn.disabled = false;
 | 
			
		||||
                loading.style.display = 'none';
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -7,6 +7,7 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.huangwc.com/pig/pig-farm-controller/internal/api/middleware"
 | 
			
		||||
@@ -63,7 +64,7 @@ type API struct {
 | 
			
		||||
// 初始化Gin引擎和相关配置
 | 
			
		||||
func NewAPI(cfg *config.Config, userRepo repository.UserRepo, operationHistoryRepo repository.OperationHistoryRepo, deviceControlRepo repository.DeviceControlRepo, deviceRepo repository.DeviceRepo, websocketService *service.WebSocketService) *API {
 | 
			
		||||
	// 设置Gin为发布模式
 | 
			
		||||
	gin.SetMode(gin.ReleaseMode)
 | 
			
		||||
	gin.SetMode(gin.DebugMode)
 | 
			
		||||
 | 
			
		||||
	// 创建Gin引擎实例
 | 
			
		||||
	engine := gin.New()
 | 
			
		||||
@@ -203,6 +204,26 @@ func (a *API) setupRoutes() {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 使用NoRoute处理器处理前端路由
 | 
			
		||||
	a.engine.NoRoute(func(c *gin.Context) {
 | 
			
		||||
		path := c.Request.URL.Path
 | 
			
		||||
 | 
			
		||||
		// 判断是否为API路径
 | 
			
		||||
		if strings.HasPrefix(path, "/api/") || strings.HasPrefix(path, "/ws/") {
 | 
			
		||||
			// API路径返回404
 | 
			
		||||
			c.JSON(http.StatusNotFound, gin.H{
 | 
			
		||||
				"error": "API路径未找到",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 其他路径提供前端静态文件服务
 | 
			
		||||
		c.File("./frontend/index.html")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// 静态文件服务 - 提供前端静态资源
 | 
			
		||||
	a.engine.Static("/static", "./frontend/static")
 | 
			
		||||
 | 
			
		||||
	// TODO: 添加更多路由
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -65,16 +65,8 @@ func (c *Controller) Register(ctx *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 对密码进行哈希处理
 | 
			
		||||
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Error("密码哈希处理失败: " + err.Error())
 | 
			
		||||
		controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "用户注册失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 创建用户
 | 
			
		||||
	user, err := c.userRepo.CreateUser(req.Username, string(hashedPassword))
 | 
			
		||||
	user, err := c.userRepo.CreateUser(req.Username, req.Password)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.logger.Error("创建用户失败: " + err.Error())
 | 
			
		||||
		controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "用户注册失败")
 | 
			
		||||
 
 | 
			
		||||
@@ -52,49 +52,7 @@ type Application struct {
 | 
			
		||||
// NewApplication 创建并返回一个新的核心应用实例
 | 
			
		||||
// 初始化所有核心组件
 | 
			
		||||
func NewApplication(cfg *config.Config) *Application {
 | 
			
		||||
	// 从配置中获取数据库连接字符串
 | 
			
		||||
	connectionString := cfg.GetDatabaseConnectionString()
 | 
			
		||||
 | 
			
		||||
	// 从配置中获取连接池参数
 | 
			
		||||
	maxOpenConns := cfg.Database.MaxOpenConns
 | 
			
		||||
	maxIdleConns := cfg.Database.MaxIdleConns
 | 
			
		||||
	connMaxLifetime := cfg.Database.ConnMaxLifetime
 | 
			
		||||
 | 
			
		||||
	// 初始化存储组件
 | 
			
		||||
	store := db.NewStorage(connectionString, maxOpenConns, maxIdleConns, connMaxLifetime)
 | 
			
		||||
 | 
			
		||||
	// 初始化用户仓库
 | 
			
		||||
	userRepo := repository.NewUserRepo(store.GetDB())
 | 
			
		||||
 | 
			
		||||
	// 初始化操作历史仓库
 | 
			
		||||
	operationHistoryRepo := repository.NewOperationHistoryRepo(store.GetDB())
 | 
			
		||||
 | 
			
		||||
	// 初始化设备控制仓库
 | 
			
		||||
	deviceControlRepo := repository.NewDeviceControlRepo(store.GetDB())
 | 
			
		||||
 | 
			
		||||
	// 初始化设备仓库
 | 
			
		||||
	deviceRepo := repository.NewDeviceRepo(store.GetDB())
 | 
			
		||||
 | 
			
		||||
	// 初始化WebSocket服务
 | 
			
		||||
	websocketService := service.NewWebSocketService()
 | 
			
		||||
	// 设置WebSocket超时时间
 | 
			
		||||
	websocketService.SetDefaultTimeout(cfg.GetWebSocketTimeout())
 | 
			
		||||
 | 
			
		||||
	// 初始化API组件
 | 
			
		||||
	apiInstance := api.NewAPI(cfg, userRepo, operationHistoryRepo, deviceControlRepo, deviceRepo, websocketService)
 | 
			
		||||
 | 
			
		||||
	// 初始化任务执行器组件(使用5个工作协程)
 | 
			
		||||
	taskExecutor := task.NewExecutor(5)
 | 
			
		||||
 | 
			
		||||
	return &Application{
 | 
			
		||||
		Storage:              store,
 | 
			
		||||
		API:                  apiInstance,
 | 
			
		||||
		TaskExecutor:         taskExecutor,
 | 
			
		||||
		UserRepo:             userRepo,
 | 
			
		||||
		OperationHistoryRepo: operationHistoryRepo,
 | 
			
		||||
		DeviceControlRepo:    deviceControlRepo,
 | 
			
		||||
		DeviceRepo:           deviceRepo,
 | 
			
		||||
		WebSocketService:     websocketService,
 | 
			
		||||
		Config: cfg,
 | 
			
		||||
		logger: logs.NewLogger(),
 | 
			
		||||
	}
 | 
			
		||||
@@ -103,12 +61,46 @@ func NewApplication(cfg *config.Config) *Application {
 | 
			
		||||
// Start 启动核心应用
 | 
			
		||||
// 按正确顺序启动所有核心组件
 | 
			
		||||
func (app *Application) Start() error {
 | 
			
		||||
	// 从配置中获取数据库连接字符串
 | 
			
		||||
	connectionString := app.Config.GetDatabaseConnectionString()
 | 
			
		||||
 | 
			
		||||
	// 从配置中获取连接池参数
 | 
			
		||||
	maxOpenConns := app.Config.Database.MaxOpenConns
 | 
			
		||||
	maxIdleConns := app.Config.Database.MaxIdleConns
 | 
			
		||||
	connMaxLifetime := app.Config.Database.ConnMaxLifetime
 | 
			
		||||
 | 
			
		||||
	// 初始化存储组件
 | 
			
		||||
	app.Storage = db.NewStorage(connectionString, maxOpenConns, maxIdleConns, connMaxLifetime)
 | 
			
		||||
 | 
			
		||||
	// 启动存储组件
 | 
			
		||||
	if err := app.Storage.Connect(); err != nil {
 | 
			
		||||
		return fmt.Errorf("存储连接失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	app.logger.Info("存储连接成功")
 | 
			
		||||
 | 
			
		||||
	// 初始化用户仓库
 | 
			
		||||
	app.UserRepo = repository.NewUserRepo(app.Storage.GetDB())
 | 
			
		||||
 | 
			
		||||
	// 初始化操作历史仓库
 | 
			
		||||
	app.OperationHistoryRepo = repository.NewOperationHistoryRepo(app.Storage.GetDB())
 | 
			
		||||
 | 
			
		||||
	// 初始化设备控制仓库
 | 
			
		||||
	app.DeviceControlRepo = repository.NewDeviceControlRepo(app.Storage.GetDB())
 | 
			
		||||
 | 
			
		||||
	// 初始化设备仓库
 | 
			
		||||
	app.DeviceRepo = repository.NewDeviceRepo(app.Storage.GetDB())
 | 
			
		||||
 | 
			
		||||
	// 初始化WebSocket服务
 | 
			
		||||
	app.WebSocketService = service.NewWebSocketService()
 | 
			
		||||
	// 设置WebSocket超时时间
 | 
			
		||||
	app.WebSocketService.SetDefaultTimeout(app.Config.GetWebSocketTimeout())
 | 
			
		||||
 | 
			
		||||
	// 初始化API组件
 | 
			
		||||
	app.API = api.NewAPI(app.Config, app.UserRepo, app.OperationHistoryRepo, app.DeviceControlRepo, app.DeviceRepo, app.WebSocketService)
 | 
			
		||||
 | 
			
		||||
	// 初始化任务执行器组件(使用5个工作协程)
 | 
			
		||||
	app.TaskExecutor = task.NewExecutor(5)
 | 
			
		||||
 | 
			
		||||
	// 启动API组件
 | 
			
		||||
	if err := app.API.Start(); err != nil {
 | 
			
		||||
		return fmt.Errorf("API启动失败: %v", err)
 | 
			
		||||
@@ -116,8 +108,8 @@ func (app *Application) Start() error {
 | 
			
		||||
	app.logger.Info("API启动成功")
 | 
			
		||||
 | 
			
		||||
	// 启动任务执行器组件
 | 
			
		||||
	app.logger.Info("启动任务执行器")
 | 
			
		||||
	app.TaskExecutor.Start()
 | 
			
		||||
	app.logger.Info("任务执行器启动成功")
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -131,8 +123,8 @@ func (app *Application) Stop() error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 停止任务执行器组件
 | 
			
		||||
	app.logger.Info("停止任务执行器")
 | 
			
		||||
	app.TaskExecutor.Stop()
 | 
			
		||||
	app.logger.Info("任务执行器已停止")
 | 
			
		||||
 | 
			
		||||
	// 停止存储组件
 | 
			
		||||
	if err := app.Storage.Disconnect(); err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,5 +24,6 @@ type Storage interface {
 | 
			
		||||
// 根据配置返回相应的存储实现
 | 
			
		||||
func NewStorage(connectionString string, maxOpenConns, maxIdleConns, connMaxLifetime int) Storage {
 | 
			
		||||
	// 当前默认返回PostgreSQL存储实现
 | 
			
		||||
	return NewPostgresStorage(connectionString, maxOpenConns, maxIdleConns, connMaxLifetime)
 | 
			
		||||
	s := NewPostgresStorage(connectionString, maxOpenConns, maxIdleConns, connMaxLifetime)
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user