feat: 网关管理页面 + 设备表单网关下拉联动

- 新增 src/api/gateway.js(网关 CRUD + 测试连接 + options API + 枚举映射)
- 新增 src/views/config/gateway/Index.vue(网关管理页面:列表/新增/编辑/测试连接/详情抽屉+下挂设备)
- 修改 src/views/config/device/Index.vue(GatewayCode 文本输入改为 GatewayId 下拉选择,列表列显示 GatewayName)
- 新增 src/api/product.js、src/api/thingPoint.js(产品/物模型点 API)
- 新增 src/components/thing/(PointSendForm、ThingPointTable、ThingAlarmPanel 共享组件)
This commit is contained in:
2026-09-04 17:43:36 +08:00
parent a72e2e6ff5
commit 240ede9769
8 changed files with 1072 additions and 11 deletions
+154
View File
@@ -0,0 +1,154 @@
<template>
<div class="point-send-form">
<div class="ps-toolbar">
<span class="ps-count">
{{ points.length }} 个点可下发 {{ writablePoints.length }}
<span v-if="points.length" class="ps-tip">只读点不支持下发Int32/Float 从地址起连写 2 个寄存器</span>
</span>
<el-button link type="primary" size="small" :loading="loading" @click="fetchPoints">刷新</el-button>
</div>
<el-table :data="points" v-loading="loading" size="small" border stripe>
<el-table-column prop="Code" label="编码" width="120" />
<el-table-column prop="Name" label="名称" min-width="110" show-overflow-tooltip />
<el-table-column label="寄存器" width="90">
<template #default="{ row }">{{ registerLabel(row.RegisterType) }}</template>
</el-table-column>
<el-table-column label="地址" width="70" align="center">
<template #default="{ row }">0x{{ dec2hex(row.Address) }}</template>
</el-table-column>
<el-table-column label="读写" width="60" align="center">
<template #default="{ row }">{{ rwLabel(row.Rw) }}</template>
</el-table-column>
<el-table-column label="状态" width="84" align="center">
<template #default="{ row }">
<el-tag v-if="row.Rw === 1" size="small" type="info">只读</el-tag>
<el-tag v-else size="small" type="success">可下发</el-tag>
</template>
</el-table-column>
<el-table-column label="下发值" width="150">
<template #default="{ row }">
<el-switch
v-if="row.DataType === 1"
:model-value="row._sendValue === 1"
active-text=""
@change="row._sendValue = $event ? 1 : 0"
/>
<el-input-number
v-else
v-model="row._sendValue"
:precision="row.DataType === 5 ? 1 : 0"
:step="1"
:controls="false"
size="small"
style="width: 100%"
/>
</template>
</el-table-column>
<el-table-column label="单位" width="60" align="center">
<template #default="{ row }">{{ row.Unit || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="70" fixed="right" align="center">
<template #default="{ row }">
<el-button
link type="primary"
:disabled="row.Rw === 1"
@click="onSend(row)"
>下发</el-button>
</template>
</el-table-column>
</el-table>
<!-- 空状态区分无点 / 点均不可下发 -->
<el-empty
v-if="!loading && points.length === 0"
description="该设备暂无物模型点,请先在「物模型点」标签页为该设备添加数据点"
:image-size="60"
/>
<el-alert
v-else-if="!loading && writablePoints.length === 0"
title="已添加的点均为只读,暂无可下发点(可将点读写属性改为「只写/读写」)"
type="info"
:closable="false"
class="ps-feedback"
/>
<!-- 下发反馈 -->
<el-alert v-if="feedback.text" :title="feedback.text" :type="feedback.type" :closable="false" class="ps-feedback" />
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getThingPoints, registerLabel, rwLabel } from '@/api/thingPoint'
import { sendDeviceCommand } from '@/api/device'
const props = defineProps({
deviceId: { type: String, required: true }
})
const loading = ref(false)
const points = ref([])
const feedback = ref({ text: '', type: 'success' })
// 只读(1)的点不能下发;只写/读写都可以(Int32/Float 走 FC16 连写 2 个寄存器,已支持)
const writablePoints = computed(() => points.value.filter(p => p.Rw !== 1))
function dec2hex(n) {
return (Number(n) || 0).toString(16).toUpperCase().padStart(4, '0')
}
async function fetchPoints() {
loading.value = true
try {
const pts = (await getThingPoints(2, props.deviceId)) || []
// 显示设备全部点(含只读),便于与「物模型点」标签页对照
points.value = pts.map(p => ({ ...p, _sendValue: p.DataType === 1 ? 1 : 0 }))
} catch (e) {
points.value = []
} finally {
loading.value = false
}
}
async function onSend(row) {
try {
await sendDeviceCommand(props.deviceId, {
Id: props.deviceId,
PointId: row.Id,
Value: row._sendValue ?? 0,
IsSimulated: false
})
// res 为后端返回的 Data(无数据时返回 undefined),消息提示走统一处理
feedback.value = { text: `已下发至「${row.Name || row.Code}`, type: 'success' }
ElMessage.success(`指令已下发:${row.Name || row.Code}`)
} catch (e) {
feedback.value = { text: e?.message || '下发失败', type: 'error' }
}
}
watch(() => props.deviceId, (val) => { if (val) fetchPoints() })
onMounted(() => { if (props.deviceId) fetchPoints() })
defineExpose({ refresh: fetchPoints })
</script>
<style scoped>
.ps-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.ps-count {
font-size: 12px;
color: #909399;
}
.ps-tip {
color: #c0c4cc;
}
.ps-feedback {
margin-top: 12px;
}
</style>