2025-09-19 14:25:20 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="device-list">
|
|
|
|
|
|
<el-card>
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="card-header">
|
2025-09-19 15:15:20 +08:00
|
|
|
|
<h2 class="page-title">设备管理</h2>
|
2025-09-19 14:25:20 +08:00
|
|
|
|
<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="devices" style="width: 100%">
|
|
|
|
|
|
<el-table-column prop="id" label="设备ID" width="180" />
|
|
|
|
|
|
<el-table-column prop="name" label="设备名称" width="180" />
|
|
|
|
|
|
<el-table-column prop="type" label="设备类型" />
|
|
|
|
|
|
<el-table-column prop="status" label="状态">
|
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<el-tag :type="scope.row.status === 'online' ? 'success' : 'danger'">
|
|
|
|
|
|
{{ scope.row.status === 'online' ? '在线' : '离线' }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="lastUpdate" label="最后更新" />
|
|
|
|
|
|
<el-table-column label="操作">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
2025-09-20 16:37:26 +08:00
|
|
|
|
<!-- 设备表单对话框 -->
|
|
|
|
|
|
<DeviceForm
|
|
|
|
|
|
v-model:visible="dialogVisible"
|
|
|
|
|
|
:device-data="currentDevice"
|
|
|
|
|
|
:is-edit="isEdit"
|
|
|
|
|
|
@success="onDeviceSuccess"
|
|
|
|
|
|
@cancel="dialogVisible = false"
|
|
|
|
|
|
/>
|
2025-09-19 14:25:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import deviceService from '../services/deviceService.js';
|
2025-09-20 16:37:26 +08:00
|
|
|
|
import DeviceForm from './DeviceForm.vue';
|
2025-09-19 14:25:20 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'DeviceList',
|
2025-09-20 16:37:26 +08:00
|
|
|
|
components: {
|
|
|
|
|
|
DeviceForm
|
|
|
|
|
|
},
|
2025-09-19 14:25:20 +08:00
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
devices: [],
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
error: null,
|
|
|
|
|
|
saving: false,
|
|
|
|
|
|
dialogVisible: false,
|
2025-09-20 16:37:26 +08:00
|
|
|
|
currentDevice: {},
|
2025-09-19 14:25:20 +08:00
|
|
|
|
isEdit: false
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
async mounted() {
|
|
|
|
|
|
await this.loadDevices();
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
// 加载设备列表
|
|
|
|
|
|
async loadDevices() {
|
|
|
|
|
|
this.loading = true;
|
|
|
|
|
|
this.error = null;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await deviceService.getDevices();
|
|
|
|
|
|
this.devices = data.map(device => ({
|
|
|
|
|
|
...device,
|
|
|
|
|
|
// 格式化数据显示
|
|
|
|
|
|
type: this.formatDeviceType(device.type),
|
2025-09-19 23:56:30 +08:00
|
|
|
|
lastUpdate: device.updated_at || '-'
|
2025-09-19 14:25:20 +08:00
|
|
|
|
}));
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
this.error = err.message || '未知错误';
|
|
|
|
|
|
console.error('加载设备列表失败:', err);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化设备类型显示
|
|
|
|
|
|
formatDeviceType(type) {
|
|
|
|
|
|
const typeMap = {
|
|
|
|
|
|
'sensor': '传感器',
|
|
|
|
|
|
'controller': '控制器',
|
|
|
|
|
|
'camera': '摄像头'
|
|
|
|
|
|
};
|
|
|
|
|
|
return typeMap[type] || type;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
addDevice() {
|
2025-09-20 16:37:26 +08:00
|
|
|
|
this.currentDevice = {};
|
2025-09-19 14:25:20 +08:00
|
|
|
|
this.isEdit = false;
|
|
|
|
|
|
this.dialogVisible = true;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
editDevice(device) {
|
|
|
|
|
|
// 注意:这里需要将显示值转换回API值
|
|
|
|
|
|
const typeMap = {
|
|
|
|
|
|
'传感器': 'sensor',
|
|
|
|
|
|
'控制器': 'controller',
|
|
|
|
|
|
'摄像头': 'camera'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.currentDevice = {
|
|
|
|
|
|
...device,
|
|
|
|
|
|
type: typeMap[device.type] || device.type
|
|
|
|
|
|
};
|
|
|
|
|
|
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 || '未知错误'));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-09-20 16:37:26 +08:00
|
|
|
|
// 设备操作成功回调
|
|
|
|
|
|
async onDeviceSuccess() {
|
|
|
|
|
|
this.$message.success(this.isEdit ? '设备更新成功' : '设备添加成功');
|
|
|
|
|
|
this.dialogVisible = false;
|
|
|
|
|
|
// 重新加载设备列表
|
|
|
|
|
|
await this.loadDevices();
|
2025-09-19 14:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2025-09-19 15:15:20 +08:00
|
|
|
|
.device-list {
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-19 14:25:20 +08:00
|
|
|
|
.card-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
2025-09-19 15:15:20 +08:00
|
|
|
|
padding: 15px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-title {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
|
font-weight: bold;
|
2025-09-19 14:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dialog-footer {
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading {
|
|
|
|
|
|
padding: 20px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.error {
|
|
|
|
|
|
padding: 20px 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.retry-btn {
|
|
|
|
|
|
margin-top: 15px;
|
|
|
|
|
|
}
|
2025-09-19 15:15:20 +08:00
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.device-list {
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-header {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-19 14:25:20 +08:00
|
|
|
|
</style>
|