1. 设备model增加地址字段, 用于保存硬件地址

2. 优化前端界面
This commit is contained in:
2025-09-08 20:28:26 +08:00
parent 9d36ae6b00
commit bbda4f4fca
18 changed files with 16039 additions and 1115 deletions

View File

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

View File

@@ -1,784 +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;
}
@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()">&times;</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>
</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,
created_at: "2023-01-01T00:00:00Z"
},
{
id: 2,
name: "A区主控",
type: "pig_pen_controller",
parent_id: 1,
created_at: "2023-01-02T00:00:00Z"
},
{
id: 3,
name: "A区1号风机",
type: "fan",
parent_id: 2,
created_at: "2023-01-03T00:00:00Z"
},
{
id: 4,
name: "A区2号风机",
type: "fan",
parent_id: 2,
created_at: "2023-01-03T00:00:00Z"
},
{
id: 5,
name: "B区主控",
type: "pig_pen_controller",
parent_id: 1,
created_at: "2023-01-02T00:00:00Z"
},
{
id: 6,
name: "B区水帘",
type: "water_curtain",
parent_id: 5,
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>
</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>
</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>
</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;
if (!name || !type) {
alert('请填写必填字段');
return;
}
const deviceData = {
name: name,
type: type,
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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

21
frontend/dist/assets/index.3d7f01fe.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1
frontend/node_modules/.vite/deps_temp/package.json generated vendored Normal file
View File

@@ -0,0 +1 @@
{"type":"module"}

2768
frontend/node_modules/.vite/deps_temp/vue-router.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

343
frontend/node_modules/.vite/deps_temp/vue.js generated vendored Normal file
View 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
View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

@@ -13,8 +13,74 @@
<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>
<div v-else>
<!-- 中继设备 -->
<div
v-for="relay in relayDevices"
:key="relay.id"
class="tree-node relay-node"
>
<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 class="node-actions">
<button class="action-btn edit-btn" @click.stop="editDevice(relay)">编辑</button>
<button class="action-btn delete-btn" @click.stop="deleteDevice(relay.id)">删除</button>
</div>
</div>
<!-- 控制器设备 -->
<div v-show="expandedNodes.has(relay.id)" class="children-container">
<div
v-for="controller in getControllerDevices(relay.id)"
:key="controller.id"
class="tree-node controller-node"
>
<div class="node-header" @click="toggleNode(controller.id)">
<div class="node-info">
<span class="toggle-icon">{{ expandedNodes.has(controller.id) ? '▼' : '►' }}</span>
<span class="node-title">{{ controller.name }}</span>
<span class="node-type controller-type">{{ getDeviceTypeText(controller.type) }}</span>
<span v-if="controller.address" class="node-address">[{{ controller.address }}]</span>
</div>
<div class="node-actions">
<button class="action-btn edit-btn" @click.stop="editDevice(controller)">编辑</button>
<button class="action-btn delete-btn" @click.stop="deleteDevice(controller.id)">删除</button>
</div>
</div>
<!-- 叶子设备 -->
<div v-show="expandedNodes.has(controller.id)" class="children-container">
<div
v-for="leaf in getLeafDevices(controller.id)"
:key="leaf.id"
class="tree-node device-node"
>
<div class="node-header">
<div class="node-info">
<span class="node-title">{{ leaf.name }}</span>
<span class="node-type device-type">{{ getDeviceTypeText(leaf.type) }}</span>
<span v-if="leaf.address" class="node-address">[{{ leaf.address }}]</span>
</div>
<div class="node-actions">
<button class="action-btn edit-btn" @click.stop="editDevice(leaf)">编辑</button>
<button class="action-btn delete-btn" @click.stop="deleteDevice(leaf.id)">删除</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</main> </main>
@@ -43,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">
@@ -80,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: {
@@ -104,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 =>
@@ -173,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
}, },
@@ -182,9 +281,32 @@ export default {
editDevice(device) { editDevice(device) {
this.editingDevice = device this.editingDevice = device
this.deviceForm = { ...device } this.deviceForm = { ...device }
// 如果是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
@@ -207,7 +329,10 @@ export default {
const deviceData = { const deviceData = {
name: this.deviceForm.name, name: this.deviceForm.name,
type: this.deviceForm.type, type: this.deviceForm.type,
parent_id: this.deviceForm.parent_id 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) {
@@ -280,6 +405,15 @@ export default {
if (this.deviceForm.type === 'relay') { if (this.deviceForm.type === 'relay') {
this.deviceForm.parent_id = null this.deviceForm.parent_id = null
} }
},
// 切换节点展开/折叠
toggleNode(nodeId) {
if (this.expandedNodes.has(nodeId)) {
this.expandedNodes.delete(nodeId)
} else {
this.expandedNodes.add(nodeId)
}
} }
} }
} }
@@ -290,20 +424,25 @@ export default {
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #f5f7fa;
} }
.header { .header {
background: #2c3e50; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
padding: 1rem; padding: 1rem 2rem;
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 {
margin: 0; margin: 0;
font-size: 1.5rem; font-size: 1.5rem;
display: flex;
align-items: center;
gap: 10px;
} }
.user-info { .user-info {
@@ -313,114 +452,160 @@ export default {
} }
.logout-btn { .logout-btn {
background: #e74c3c; background: rgba(255, 255, 255, 0.2);
color: white; color: white;
border: none; border: 1px solid rgba(255, 255, 255, 0.3);
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease;
} }
.logout-btn:hover { .logout-btn:hover {
background: #c0392b; background: rgba(255, 255, 255, 0.3);
} }
.main-content { .main-content {
flex: 1; flex: 1;
padding: 1rem; padding: 1.5rem;
overflow-y: auto; overflow-y: auto;
} }
.toolbar { .toolbar {
margin-bottom: 1rem; margin-bottom: 1.5rem;
display: flex;
justify-content: flex-end;
} }
.btn { .btn {
padding: 0.5rem 1rem; padding: 0.75rem 1.5rem;
border: none; border: none;
border-radius: 4px; border-radius: 6px;
cursor: pointer; cursor: pointer;
font-size: 1rem; font-size: 1rem;
font-weight: 500;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
} }
.btn-primary { .btn-primary {
background: #3498db; 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: #2980b9; transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
} }
.btn-secondary { .btn-secondary {
background: #95a5a6; background: #f5f5f5;
color: white; color: #333;
border: 1px solid #ddd;
} }
.btn-secondary:hover { .btn-secondary:hover {
background: #7f8c8d; background: #e0e0e0;
} }
.device-tree { .device-tree {
background: white; background: white;
border-radius: 4px; border-radius: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1rem; padding: 1.5rem;
overflow: hidden;
} }
.tree-node { .tree-node {
margin-bottom: 0.5rem; margin-bottom: 0.75rem;
padding: 0.5rem; border-radius: 8px;
border-radius: 4px; transition: all 0.3s ease;
border-left: 3px solid #3498db; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.tree-node:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
} }
.tree-node.relay-node { .tree-node.relay-node {
background: #ecf0f1; background: #fff3e0;
border-left-color: #e74c3c; border-left: 4px solid #ff9800;
} }
.tree-node.controller-node { .tree-node.controller-node {
background: #f8f9fa; background: #e3f2fd;
border-left-color: #f39c12; border-left: 4px solid #2196f3;
margin-left: 1rem; margin-left: 1.5rem;
} }
.tree-node.device-node { .tree-node.device-node {
background: #fff; background: #e8f5e9;
border-left-color: #2ecc71; border-left: 4px solid #4caf50;
margin-left: 2rem; 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: 1rem;
cursor: pointer;
}
.node-header:hover {
background-color: rgba(0, 0, 0, 0.02);
}
.node-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.toggle-icon {
width: 20px;
text-align: center;
font-size: 0.8rem;
color: #666;
} }
.node-title { .node-title {
font-weight: bold; font-weight: 600;
color: #333;
font-size: 1.1rem;
}
.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: 0.8rem; font-size: 0.8rem;
padding: 0.2rem 0.5rem; padding: 0.3rem 0.6rem;
border-radius: 4px; border-radius: 12px;
margin: 0 0.5rem; font-weight: 500;
text-transform: uppercase;
} }
.relay-type { .relay-type {
background: #e74c3c; background: #ff9800;
color: white; color: white;
} }
.controller-type { .controller-type {
background: #f39c12; background: #2196f3;
color: white; color: white;
} }
.device-type { .device-type {
background: #2ecc71; background: #4caf50;
color: white; color: white;
} }
@@ -430,29 +615,38 @@ export default {
} }
.action-btn { .action-btn {
padding: 0.2rem 0.5rem; padding: 0.4rem 0.8rem;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-size: 0.9rem; font-size: 0.85rem;
transition: all 0.2s ease;
} }
.edit-btn { .edit-btn {
background: #3498db; background: #1976d2;
color: white; color: white;
} }
.edit-btn:hover { .edit-btn:hover {
background: #2980b9; background: #1565c0;
transform: translateY(-1px);
} }
.delete-btn { .delete-btn {
background: #e74c3c; background: #d32f2f;
color: white; color: white;
} }
.delete-btn:hover { .delete-btn:hover {
background: #c0392b; 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 {
@@ -461,37 +655,57 @@ export default {
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgba(0,0,0,0.5); background: rgba(0, 0, 0, 0.5);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 1000; 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: 4px; border-radius: 10px;
width: 90%; width: 90%;
max-width: 500px; max-width: 500px;
box-shadow: 0 4px 8px 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 {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 1rem; padding: 1.5rem;
border-bottom: 1px solid #eee; 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: 1.5rem; font-size: 1.8rem;
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
width: 30px; width: 30px;
@@ -499,42 +713,85 @@ export default {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #999;
transition: color 0.2s ease;
} }
.close-btn:hover { .close-btn:hover {
background: #f5f5f5; color: #333;
border-radius: 50%;
} }
.modal-body { .modal-body {
padding: 1rem; padding: 1.5rem;
} }
.form-group { .form-group {
margin-bottom: 1rem; margin-bottom: 1.5rem;
} }
.form-group label { .form-group label {
display: block; display: block;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-weight: bold; font-weight: 500;
color: #333;
} }
.form-group input, .form-group input,
.form-group select { .form-group select {
width: 100%; width: 100%;
padding: 0.5rem; padding: 0.8rem;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 6px;
font-size: 1rem; font-size: 1rem;
box-sizing: border-box; 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: 1rem; 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: 0.5rem; gap: 0.75rem;
}
.no-devices {
text-align: center;
color: #7f8c8d;
padding: 3rem;
font-style: italic;
font-size: 1.1rem;
}
@media (max-width: 768px) {
.main-content {
padding: 1rem;
}
.header {
padding: 1rem;
}
.node-header {
flex-direction: column;
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>

View File

@@ -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,12 +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,
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 {
@@ -175,6 +224,14 @@ 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: 设备状态应该由系统自动获取,而不是由用户指定
// 这里保持设备原有状态,后续需要实现自动状态检测
// 设备状态现在只在内存中维护,不持久化到数据库 // 设备状态现在只在内存中维护,不持久化到数据库
if err := c.deviceRepo.Update(device); err != nil { if err := c.deviceRepo.Update(device); err != nil {

View File

@@ -3,6 +3,9 @@
package model package model
import ( import (
"fmt"
"strconv"
"strings"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
@@ -42,6 +45,11 @@ 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"`
// Address 设备地址普通设备的485总线地址或区域主控的Lora地址中继设备不需要
// 格式:对于普通设备,可以是"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"`
@@ -57,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