319 lines
8.2 KiB
Vue
319 lines
8.2 KiB
Vue
<template>
|
|
<div class="device-list">
|
|
<el-card>
|
|
<template #header>
|
|
<div class="card-header">
|
|
<div class="title-container">
|
|
<h2 class="page-title">设备管理</h2>
|
|
<el-button type="text" @click="loadDevices" class="refresh-btn" title="刷新设备列表">
|
|
<el-icon :size="20"><Refresh /></el-icon>
|
|
</el-button>
|
|
</div>
|
|
<el-button type="primary" @click="addDevice">添加设备</el-button>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 加载状态 -->
|
|
<div v-if="loading" class="loading">
|
|
<el-skeleton animated />
|
|
</div>
|
|
|
|
<!-- 错误状态 -->
|
|
<div v-else-if="error" class="error">
|
|
<el-alert
|
|
title="获取设备数据失败"
|
|
:description="error"
|
|
type="error"
|
|
show-icon
|
|
closable
|
|
@close="error = null"
|
|
/>
|
|
<el-button type="primary" @click="loadDevices" class="retry-btn">重新加载</el-button>
|
|
</div>
|
|
|
|
<!-- 设备列表 -->
|
|
<el-table
|
|
v-else
|
|
:data="tableData"
|
|
style="width: 100%"
|
|
:fit="true"
|
|
table-layout="auto"
|
|
row-key="id"
|
|
default-expand-all
|
|
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
|
:row-class-name="tableRowClassName"
|
|
:highlight-current-row="false"
|
|
:scrollbar-always-on="true"
|
|
@sort-change="handleSortChange">
|
|
<el-table-column width="40"></el-table-column>
|
|
<el-table-column prop="id" label="设备ID" min-width="100" sortable="custom" />
|
|
<el-table-column prop="name" label="设备名称" min-width="120" sortable="custom" />
|
|
<el-table-column prop="type" label="设备类型" min-width="100">
|
|
<template #default="scope">
|
|
{{ formatDeviceType(scope.row.type) }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="sub_type" label="设备子类型" min-width="100">
|
|
<template #default="scope">
|
|
{{ formatDeviceSubType(scope.row.sub_type) }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="location" label="设备地址描述" min-width="150" />
|
|
<el-table-column label="操作" min-width="120" align="center">
|
|
<template #default="scope">
|
|
<el-button size="small" @click="editDevice(scope.row)">编辑</el-button>
|
|
<el-button size="small" type="danger" @click="deleteDevice(scope.row)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-card>
|
|
|
|
<!-- 设备表单对话框 -->
|
|
<DeviceForm
|
|
v-model:visible="dialogVisible"
|
|
:device-data="currentDevice"
|
|
:is-edit="isEdit"
|
|
@success="onDeviceSuccess"
|
|
@cancel="dialogVisible = false"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { Refresh } from '@element-plus/icons-vue';
|
|
import deviceService from '../services/deviceService.js';
|
|
import DeviceForm from './DeviceForm.vue';
|
|
|
|
export default {
|
|
name: 'DeviceList',
|
|
components: {
|
|
DeviceForm,
|
|
Refresh // 导入刷新图标组件
|
|
},
|
|
data() {
|
|
return {
|
|
tableData: [], // 树形表格数据
|
|
originalTableData: [], // 存储原始未排序的树形数据
|
|
allDevices: [], // 存储所有设备用于构建树形结构
|
|
loading: false,
|
|
error: null,
|
|
saving: false,
|
|
dialogVisible: false,
|
|
currentDevice: {},
|
|
isEdit: false
|
|
};
|
|
},
|
|
async mounted() {
|
|
await this.loadDevices();
|
|
},
|
|
methods: {
|
|
// 加载设备列表
|
|
async loadDevices() {
|
|
this.loading = true;
|
|
this.error = null;
|
|
|
|
try {
|
|
const data = await deviceService.getDevices();
|
|
this.allDevices = data;
|
|
this.tableData = this.buildTreeData(data);
|
|
this.originalTableData = [...this.tableData]; // 保存原始顺序
|
|
} catch (err) {
|
|
this.error = err.message || '未知错误';
|
|
console.error('加载设备列表失败:', err);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
// 处理表格排序
|
|
handleSortChange({ prop, order }) {
|
|
if (!order) {
|
|
// 如果取消排序,则恢复原始顺序
|
|
this.tableData = [...this.originalTableData];
|
|
return;
|
|
}
|
|
|
|
const sortFactor = order === 'ascending' ? 1 : -1;
|
|
|
|
// 只对顶层项(区域主控)进行排序
|
|
this.tableData.sort((a, b) => {
|
|
const valA = a[prop];
|
|
const valB = b[prop];
|
|
|
|
if (valA < valB) {
|
|
return -1 * sortFactor;
|
|
}
|
|
if (valA > valB) {
|
|
return 1 * sortFactor;
|
|
}
|
|
return 0;
|
|
});
|
|
},
|
|
|
|
// 构建树形结构数据
|
|
buildTreeData(devices) {
|
|
const areaControllers = devices.filter(device => device.type === 'area_controller');
|
|
|
|
return areaControllers.map(controller => {
|
|
const children = devices.filter(device =>
|
|
device.type === 'device' && device.parent_id === controller.id
|
|
);
|
|
|
|
return {
|
|
...controller,
|
|
children: children.length > 0 ? children : undefined
|
|
};
|
|
});
|
|
},
|
|
|
|
// 格式化设备类型显示
|
|
formatDeviceType(type) {
|
|
const typeMap = {
|
|
'area_controller': '区域主控',
|
|
'device': '普通设备'
|
|
};
|
|
return typeMap[type] || type || '-';
|
|
},
|
|
|
|
// 格式化设备子类型显示
|
|
formatDeviceSubType(subType) {
|
|
const subTypeMap = {
|
|
'': '-',
|
|
'temperature': '温度传感器',
|
|
'humidity': '湿度传感器',
|
|
'ammonia': '氨气传感器',
|
|
'feed_valve': '饲料阀门',
|
|
'fan': '风扇',
|
|
'water_curtain': '水帘'
|
|
};
|
|
return subTypeMap[subType] || subType || '-';
|
|
},
|
|
|
|
addDevice() {
|
|
this.currentDevice = {};
|
|
this.isEdit = false;
|
|
this.dialogVisible = true;
|
|
},
|
|
|
|
editDevice(device) {
|
|
const processedDevice = { ...device };
|
|
|
|
if (processedDevice.properties && typeof processedDevice.properties === 'string') {
|
|
try {
|
|
processedDevice.properties = JSON.parse(processedDevice.properties);
|
|
} catch (e) {
|
|
console.error('解析properties失败:', e);
|
|
processedDevice.properties = {};
|
|
}
|
|
}
|
|
|
|
this.currentDevice = processedDevice;
|
|
this.isEdit = true;
|
|
this.dialogVisible = true;
|
|
},
|
|
|
|
async deleteDevice(device) {
|
|
try {
|
|
await this.$confirm('确认删除该设备吗?', '提示', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
});
|
|
|
|
await deviceService.deleteDevice(device.id);
|
|
this.$message.success('删除成功');
|
|
await this.loadDevices();
|
|
} catch (err) {
|
|
if (err !== 'cancel') {
|
|
this.$message.error('删除失败: ' + (err.message || '未知错误'));
|
|
}
|
|
}
|
|
},
|
|
|
|
async onDeviceSuccess() {
|
|
this.$message.success(this.isEdit ? '设备更新成功' : '设备添加成功');
|
|
this.dialogVisible = false;
|
|
await this.loadDevices();
|
|
},
|
|
|
|
tableRowClassName({ row, rowIndex }) {
|
|
if (row.type === 'area_controller') {
|
|
return 'current-row';
|
|
}
|
|
return '';
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.device-list {
|
|
padding: 20px;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 15px 0;
|
|
}
|
|
|
|
.title-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.page-title {
|
|
margin: 0;
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
line-height: 1;
|
|
}
|
|
|
|
.refresh-btn {
|
|
color: black;
|
|
background-color: transparent;
|
|
padding: 0;
|
|
width: 24px;
|
|
height: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: none;
|
|
}
|
|
|
|
.dialog-footer {
|
|
text-align: right;
|
|
}
|
|
|
|
.loading {
|
|
padding: 20px 0;
|
|
}
|
|
|
|
.error {
|
|
padding: 20px 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.retry-btn {
|
|
margin-top: 15px;
|
|
}
|
|
|
|
/* 确保区域主控设备始终高亮显示 */
|
|
:deep(.current-row) {
|
|
background-color: #f5f7fa !important;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.device-list {
|
|
padding: 10px;
|
|
}
|
|
.card-header {
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
}
|
|
</style> |