feat:设备管理页面对接接口

master
黄伟杰 2 months ago
parent dc5cb0990f
commit 4b13db5faf

@ -19,10 +19,10 @@ export interface DeviceVO {
remark: string // 备注
isEnable: boolean // 是否启用
deviceModelId: number // 关联设备模型
protocol: string // 通讯协议
sampleCycle: number // 采集周期
url: string // 端点url
username: string // 用户名
protocol: string // 通讯协议
sampleCycle: number // 采集周期
url: string // 端点url
username: string // 用户名
password: string // 密码
certificate?: string // 证书
secretKey?: string // 秘钥
@ -78,14 +78,13 @@ export interface DeviceContactModelVO {
dataUnit?: string
}
// 物联设备 API
export const DeviceApi = {
// 查询物联设备分页
getDevicePage: async (params: any) => {
return await request.get({ url: `/iot/device/page`, params })
},
// 查询物联设备
// 查询物联设备
getDeviceList: async () => {
return await request.get({ url: `/iot/device/deviceList` })
},
@ -105,7 +104,10 @@ export const DeviceApi = {
},
// 批量获取设备属性列表
getDeviceAttributeBatchList: async (params: { goviewId: number | string; orgId?: number | string }) => {
getDeviceAttributeBatchList: async (params: {
goviewId: number | string
orgId?: number | string
}) => {
return await request.get({ url: `/iot/device/device-attribute/batchList`, params })
},
// 修改物联设备
@ -156,12 +158,11 @@ export const DeviceApi = {
return await request.get({ url: `/iot/device/devicePointList` })
},
updateDeviceEnabled: async (id: number | string, enabled: string) => {
const data = { id, enabled }
updateDeviceEnabled: async (data) => {
return await request.put({ url: `/iot/device/update-enabled`, data })
},
// ==================== 子表(设备属性) ====================
// ==================== 子表(设备属性) ====================
// 获得设备属性分页
getDeviceAttributePage: async (params) => {
@ -172,6 +173,10 @@ export const DeviceApi = {
return await request.get({ url: `/iot/device/device-attribute/list?deviceId=` + deviceId })
},
getDeviceWarinningRecordPage: async (params: any) => {
return await request.get({ url: `/iot/device-warinning-record/page`, params })
},
getDeviceContactModelPage: async () => {
return await request.get({ url: `/iot/device-contact-model/page` })
},

@ -7,12 +7,12 @@
label-width="110px"
v-loading="formLoading"
>
<el-form-item label="父组织" prop="parentId">
<el-form-item label="父组织" prop="parentKey">
<el-tree-select
v-model="formData.parentId"
v-model="formData.parentKey"
:data="parentTreeOptions"
:props="treeProps"
node-key="id"
node-key="nodeKey"
check-strictly
clearable
filterable
@ -76,6 +76,8 @@ type TreeNode = {
id: string
deviceId?: string
parentId?: string
parentKey?: string
nodeKey?: string
customerId?: string
nodeType?: number
name: string
@ -103,12 +105,13 @@ const dialogTitle = computed(() =>
)
const formRef = ref()
const modelList = ref<DeviceModelVO[]>([])
const treeProps = { children: 'children', label: 'name', value: 'id' }
const treeProps = { children: 'children', label: 'name', value: 'nodeKey' }
const currentParentNode = ref<TreeNode>()
const formData = ref({
id: undefined as number | undefined,
customerId: undefined as number | undefined,
parentId: undefined as string | undefined,
parentKey: undefined as string | undefined,
nodeType: 2,
name: '',
deviceCode: '',
@ -127,7 +130,8 @@ const nodeTypeOptions = computed(() => {
})
const currentNodeTypeOptions = computed(() => {
const parentType = Number(currentParentNode.value?.nodeType || 1)
const selectedNode = findNodeByNodeKey(formData.value.parentKey) || currentParentNode.value
const parentType = Number(selectedNode?.nodeType || 1)
const allowed =
parentType === 2 ? [3, 4] : parentType === 3 ? [4] : parentType === 1 ? [2, 3, 4] : []
return nodeTypeOptions.value.filter((item) => allowed.includes(item.value))
@ -147,13 +151,20 @@ const parentTreeOptions = computed(() => {
const formRules = computed(() => {
const common = {
parentId: [{ required: true, message: '父组织不能为空', trigger: 'change' }],
parentKey: [{ required: true, message: '父组织不能为空', trigger: 'change' }],
nodeType: [{ required: true, message: '节点类型不能为空', trigger: 'change' }]
}
if (formData.value.nodeType === 4) {
return {
...common,
deviceCode: [{ required: true, message: '设备编号不能为空', trigger: 'blur' }],
deviceCode: [
{ required: true, message: '设备编号不能为空', trigger: 'blur' },
{
pattern: /^[A-Za-z_][A-Za-z0-9_]*$/,
message: '设备编号仅支持字母/数字/下划线,且必须以字母或下划线开头',
trigger: 'blur'
}
],
deviceName: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }],
deviceModelId: [{ required: true, message: '设备模型不能为空', trigger: 'change' }]
}
@ -177,6 +188,15 @@ const loadModels = async () => {
modelList.value = Array.isArray(data) ? data : []
}
watch(
() => currentNodeTypeOptions.value,
(options) => {
if (options.length && !options.find((item) => item.value === formData.value.nodeType)) {
formData.value.nodeType = options[0].value
}
}
)
watch(
() => formData.value.nodeType,
async (nodeType) => {
@ -186,7 +206,7 @@ watch(
}
)
const findNodeById = (id?: string): TreeNode | undefined => {
function findNodeById(id?: string): TreeNode | undefined {
if (!id) return undefined
const stack = [...(props.treeData || [])]
while (stack.length) {
@ -197,17 +217,41 @@ const findNodeById = (id?: string): TreeNode | undefined => {
return undefined
}
function findNodeByNodeKey(nodeKey?: string): TreeNode | undefined {
if (!nodeKey) return undefined
const stack = [...(props.treeData || [])]
while (stack.length) {
const node = stack.shift()!
if (node.nodeKey === nodeKey) return node
if (node.children?.length) stack.push(...node.children)
}
return undefined
}
function findParentNode(node?: TreeNode): TreeNode | undefined {
if (!node) return undefined
if (node.parentKey && node.parentKey !== '0') {
const parentByKey = findNodeByNodeKey(node.parentKey)
if (parentByKey) return parentByKey
}
if (node.parentId && node.parentId !== '0') {
const parentById = findNodeById(node.parentId)
if (parentById) return parentById
}
return undefined
}
const getCustomerIdByNode = (node?: TreeNode) => {
if (!node) return Number(props.customerId || '') || undefined
if (Number(node.nodeType) === 1) {
return Number(node.customerId || node.id || props.customerId || '') || undefined
return Number(node.id) || undefined
}
let current = node
let current: TreeNode | undefined = node
while (current) {
if (Number(current.nodeType) === 1) {
return Number(current.customerId || current.id || props.customerId || '') || undefined
return Number(current.id) || undefined
}
current = findNodeById(current.parentId)
current = findParentNode(current)
}
return Number(props.customerId || '') || undefined
}
@ -218,11 +262,13 @@ const open = async (type: 'create' | 'update', node?: TreeNode, defaultParentId?
if (type === 'create') {
currentParentNode.value = node
const nodeType = getDefaultNodeType(node)
const initialParentId = Number(node?.nodeType) === 1 ? '0' : defaultParentId || node?.id
const initialParentId = defaultParentId || node?.id
const initialParentKey = node?.nodeKey
formData.value = {
id: undefined,
customerId: getCustomerIdByNode(node),
parentId: initialParentId,
parentKey: initialParentKey,
nodeType,
name: '',
deviceCode: '',
@ -236,7 +282,7 @@ const open = async (type: 'create' | 'update', node?: TreeNode, defaultParentId?
formData.value.nodeType = currentNodeTypeOptions.value[0].value
}
} else {
currentParentNode.value = findNodeById(node?.parentId)
currentParentNode.value = findNodeByNodeKey(node?.parentKey) || findNodeById(node?.parentId)
const deviceId =
Number(node?.deviceId || '') ||
(Number(node?.nodeType) === 4 ? Number(node?.id || '') : undefined)
@ -244,6 +290,7 @@ const open = async (type: 'create' | 'update', node?: TreeNode, defaultParentId?
id: deviceId || Number(node?.id),
customerId: getCustomerIdByNode(currentParentNode.value || node),
parentId: node?.parentId,
parentKey: node?.parentKey || currentParentNode.value?.nodeKey,
nodeType: Number(node?.nodeType || 2),
name: node?.name || '',
deviceCode: '',
@ -273,10 +320,14 @@ const submitForm = async () => {
message.warning('未识别到客户ID请检查树节点数据')
return
}
const selectedParentNode =
findNodeByNodeKey(formData.value.parentKey) || findNodeById(formData.value.parentId)
const parentId =
formType.value === 'create' && Number(currentParentNode.value?.nodeType) === 1
Number(selectedParentNode?.nodeType) === 1 || formData.value.parentKey === '0'
? 0
: Number(formData.value.parentId)
: Number(selectedParentNode?.id || formData.value.parentId || 0)
const orgNodeId = Number(selectedParentNode?.id || formData.value.parentId || 0)
const payload = {
customerId: Number(formData.value.customerId),
parentId,
@ -300,6 +351,7 @@ const submitForm = async () => {
await DeviceApi.updateDevice(devicePayload)
message.success(t('common.updateSuccess'))
} else {
devicePayload.orgNodeId = orgNodeId
await DeviceApi.createDevice(devicePayload)
message.success(t('common.createSuccess'))
}

@ -14,7 +14,7 @@
<el-tree
ref="treeRef"
node-key="id"
node-key="nodeKey"
:data="treeData"
:props="treeProps"
highlight-current
@ -27,12 +27,9 @@
<div class="device-mgmt__treeNode">
<div class="device-mgmt__treeNodeName">
<span class="device-mgmt__treeNodeText">{{ data.name }}</span>
<template v-if="data.type === 'device'">
<span
class="device-mgmt__dot"
:class="data.online ? 'device-mgmt__dot--ok' : 'device-mgmt__dot--muted'"
></span>
</template>
<span class="device-mgmt__treeNodeType" :class="getNodeTypeClass(data.nodeType)">
{{ getNodeTypeLabel(data.nodeType) }}
</span>
</div>
<template v-if="data.type === 'device'">
<span class="device-mgmt__treeNodeMeta">{{ data.code }}</span>
@ -98,10 +95,12 @@
<el-table-column label="协议" prop="protocol" width="120" />
<el-table-column label="启用" prop="isEnable" width="90">
<template #default="{ row }">
{{ row.isEnable ? '是' : '否' }}
<el-switch
v-model="row.isEnable"
@change="(val) => handleDeviceEnableChange(row, val as boolean)"
/>
</template>
</el-table-column>
<el-table-column label="采集时间" prop="collectionTime" width="180" />
<el-table-column label="操作" fixed="right" width="190">
<template #default="{ row }">
<el-button link type="primary" @click="openForm('setting', row.id)"
@ -122,12 +121,19 @@
<span
class="device-mgmt__dot device-mgmt__dot--lg"
:class="
selectedDevice?.online ? 'device-mgmt__dot--ok' : 'device-mgmt__dot--muted'
String(selectedDeviceDetail?.status) === '1' ||
selectedDeviceDetail?.status === 'online'
? 'device-mgmt__dot--ok'
: 'device-mgmt__dot--muted'
"
></span>
<div class="device-mgmt__titleText">
<div class="device-mgmt__title">{{ selectedDevice?.name || '-' }}</div>
<div class="device-mgmt__subtitle">{{ selectedDevice?.code || '-' }}</div>
<div class="device-mgmt__title">{{
selectedDeviceDetail?.deviceName || '-'
}}</div>
<div class="device-mgmt__subtitle">{{
selectedDeviceDetail?.deviceCode || '-'
}}</div>
</div>
</div>
</div>
@ -148,47 +154,58 @@
<div class="device-mgmt__baseBlockTitle">设备信息</div>
<div class="device-mgmt__baseGrid">
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">设备编码</div>
<div class="device-mgmt__baseValue">{{ selectedDevice?.code || '-' }}</div>
<div class="device-mgmt__baseLabel">设备编号</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.deviceCode || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">设备名称</div>
<div class="device-mgmt__baseValue">{{ selectedDevice?.name || '-' }}</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.deviceName || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">设备型号</div>
<div class="device-mgmt__baseValue">{{ selectedDevice?.model || '-' }}</div>
<div class="device-mgmt__baseLabel">设备类型</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.deviceType || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">制造商</div>
<div class="device-mgmt__baseLabel">关联设备模型</div>
<div class="device-mgmt__baseValue">{{
selectedDevice?.manufacturer || '-'
selectedDeviceDetail?.deviceModelId || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">所属客户</div>
<div class="device-mgmt__baseLabel">状态</div>
<div class="device-mgmt__baseValue">{{
selectedDevice?.customerName || '-'
selectedDeviceDetail?.status || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">所属产线</div>
<div class="device-mgmt__baseLabel">运行状态</div>
<div class="device-mgmt__baseValue">{{
selectedDevice?.lineName || '-'
selectedDeviceDetail?.operatingStatus || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">安装位置</div>
<div class="device-mgmt__baseValue">{{ selectedDevice?.address || '-' }}</div>
<div class="device-mgmt__baseLabel">关联组织</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.org || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">在线状态</div>
<div
class="device-mgmt__baseValue"
:class="selectedDevice?.online ? 'device-mgmt__baseValue--success' : ''"
>
{{ selectedDevice?.online ? '在线' : '离线' }}
</div>
<div class="device-mgmt__baseLabel">客户ID</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.customerId || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">组织节点ID</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.orgNodeId || '-'
}}</div>
</div>
</div>
</div>
@ -198,15 +215,57 @@
<div class="device-mgmt__baseBlockTitle">通讯配置</div>
<div class="device-mgmt__baseGrid">
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">MQTT主题</div>
<div class="device-mgmt__baseLabel">通讯协议</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.protocol || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">端点url</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.url || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">采集周期</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.sampleCycle || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">用户名</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.username || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">密码</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.password ? '******' : '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">网关id</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.gatewayId || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">读主题</div>
<div class="device-mgmt__baseValue device-mgmt__baseValue--bg">{{
selectedDeviceDetail?.readTopic || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">写主题</div>
<div class="device-mgmt__baseValue device-mgmt__baseValue--bg">{{
selectedDevice?.mqttTopic || '-'
selectedDeviceDetail?.writeTopic || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">客户端ID</div>
<div class="device-mgmt__baseLabel">mqtt订阅主题</div>
<div class="device-mgmt__baseValue device-mgmt__baseValue--bg">{{
selectedDevice?.mqttClientId || '-'
selectedDeviceDetail?.topic || '-'
}}</div>
</div>
</div>
@ -216,21 +275,53 @@
<div class="device-mgmt__baseBlock">
<div class="device-mgmt__baseBlockTitle">其他信息</div>
<div class="device-mgmt__baseGrid">
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">是否启用</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.isEnable ? '是' : '否'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">创建时间</div>
<div class="device-mgmt__baseValue">{{
selectedDevice?.createdAt || '-'
selectedDeviceDetail?.createTime
? formatDate(selectedDeviceDetail.createTime)
: '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">采集时间</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.collectionTime
? formatDate(selectedDeviceDetail.collectionTime)
: '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">离线间隔</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.offLineDuration || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">最后上线时间</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.lastOnlineTime
? formatDate(selectedDeviceDetail.lastOnlineTime)
: '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">最后更新</div>
<div class="device-mgmt__baseLabel">设备品牌id</div>
<div class="device-mgmt__baseValue">{{
selectedDevice?.updatedAt || '-'
selectedDeviceDetail?.deviceBrandId || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem device-mgmt__baseItem--full">
<div class="device-mgmt__baseLabel">备注</div>
<div class="device-mgmt__baseValue">{{ selectedDevice?.remark || '-' }}</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.remark || '-'
}}</div>
</div>
</div>
</div>
@ -238,138 +329,156 @@
</template>
<template v-else-if="activeTab === 'realtime'">
<el-row :gutter="16">
<el-col v-for="m in realtimeMetrics" :key="m.key" :xs="24" :sm="12" :md="8" :lg="6">
<el-card shadow="never" class="device-mgmt__metricCard">
<div class="device-mgmt__metricTop">
<div class="device-mgmt__metricName">{{ m.name }}</div>
<el-tag :type="m.status === 'warn' ? 'warning' : 'success'" size="small">
{{ m.status === 'warn' ? '预警' : '正常' }}
</el-tag>
</div>
<div class="device-mgmt__metricValue">
<span class="device-mgmt__metricNumber">{{ m.value }}</span>
<span class="device-mgmt__metricUnit">{{ m.unit }}</span>
</div>
</el-card>
</el-col>
</el-row>
<el-card shadow="never" class="device-mgmt__panel mt-16px">
<div class="device-mgmt__hintRow">
<div class="device-mgmt__hintTitle">最近更新时间</div>
<div class="device-mgmt__hintValue">{{ lastRealtimeAt }}</div>
</div>
</el-card>
<div v-loading="realtimeLoading">
<el-row :gutter="16">
<el-col
v-for="m in realtimeList"
:key="m.id"
:xs="24"
:sm="12"
:md="8"
:lg="6"
class="mb-16px"
>
<el-card shadow="never" class="device-mgmt__metricCard">
<div class="device-mgmt__metricTop">
<div class="device-mgmt__metricName">{{ m.attributeName }}</div>
<el-tag :type="m.status === 'warn' ? 'warning' : 'success'" size="small">
{{ m.status === 'warn' ? '预警' : '正常' }}
</el-tag>
</div>
<div class="device-mgmt__metricValue">
<span class="device-mgmt__metricNumber">{{ m.value || '-' }}</span>
<span class="device-mgmt__metricUnit">{{ m.dataUnit || '' }}</span>
</div>
</el-card>
</el-col>
</el-row>
<Pagination
v-model:page="realtimePage"
v-model:limit="realtimePageSize"
:total="realtimeTotal"
@pagination="loadRealtimeData"
class="mt-16px"
/>
</div>
</template>
<template v-else-if="activeTab === 'history'">
<el-card shadow="never" class="device-mgmt__panel">
<el-card shadow="never" class="device-mgmt__panel" v-loading="historyLoading">
<div class="device-mgmt__filters">
<div class="device-mgmt__filter">
<div class="device-mgmt__filterLabel">参数选择</div>
<el-radio-group v-model="historyMetricKey" size="small">
<el-radio-button
<el-select v-model="historyMetricKey" size="small" class="!w-160px" clearable>
<el-option
v-for="m in historyMetricOptions"
:key="m.key"
:label="m.key"
>
{{ m.name }}
</el-radio-button>
</el-radio-group>
:label="m.name"
:value="m.key"
/>
</el-select>
</div>
<div class="device-mgmt__filter">
<div class="device-mgmt__filterLabel">时间范围</div>
<el-radio-group v-model="historyRange" size="small">
<el-radio-button label="today">今日</el-radio-button>
<el-radio-button label="week">本周</el-radio-button>
<el-radio-button label="month">本月</el-radio-button>
</el-radio-group>
<el-date-picker
v-model="historyTimeRange"
type="datetimerange"
size="small"
range-separator="-"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
class="!w-320px"
/>
</div>
<div class="device-mgmt__filterActions">
<el-button type="primary">查询</el-button>
<el-button>导出</el-button>
<el-button type="primary" @click="loadHistoryData" size="small">查询</el-button>
<el-radio-group v-model="historyViewType" size="small" class="ml-16px">
<el-radio-button label="chart">曲线</el-radio-button>
<el-radio-button label="list">列表</el-radio-button>
</el-radio-group>
<el-button size="small" class="ml-16px">导出</el-button>
</div>
</div>
<Echart :height="360" :options="historyChartOptions" />
<template v-if="historyViewType === 'chart'">
<Echart :height="400" :options="historyChartOptions" />
</template>
<template v-else>
<el-table :data="historyList" border height="400" class="mt-16px">
<el-table-column label="时间" prop="time" width="180" />
<el-table-column label="参数" prop="attributeName" />
<el-table-column label="数值" prop="value" />
<el-table-column label="单位" prop="unit" />
</el-table>
</template>
</el-card>
</template>
<template v-else>
<el-card shadow="never" class="device-mgmt__panel">
<el-card shadow="never" class="device-mgmt__panel" v-loading="alarmLoading">
<div class="device-mgmt__filters">
<div class="device-mgmt__filter">
<!-- <div class="device-mgmt__filter">
<div class="device-mgmt__filterLabel">报警类型</div>
<el-select v-model="alarmType" clearable placeholder="全部" class="!w-200px">
<el-option label="温度过高" value="temp_high" />
<el-option label="压力过高" value="pressure_high" />
<el-option label="电流过高" value="current_high" />
</el-select>
</div>
<div class="device-mgmt__filter">
</div> -->
<!-- <div class="device-mgmt__filter">
<div class="device-mgmt__filterLabel">时间范围</div>
<el-radio-group v-model="alarmRange" size="small">
<el-radio-button label="today">今日</el-radio-button>
<el-radio-button label="week">本周</el-radio-button>
<el-radio-button label="month">本月</el-radio-button>
</el-radio-group>
</div>
</div> -->
<div class="device-mgmt__filterActions">
<el-button type="primary">查询</el-button>
<!-- <el-button type="primary" @click="loadAlarmData"></el-button> -->
<el-button type="success">导出</el-button>
</div>
</div>
<el-row :gutter="16" class="mt-16px">
<el-col :xs="24" :sm="8">
<el-card shadow="never" class="device-mgmt__summaryCard">
<div class="device-mgmt__summaryValue">{{ alarmSummary.total }}</div>
<div class="device-mgmt__summaryLabel">总报警</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="8">
<el-card
shadow="never"
class="device-mgmt__summaryCard device-mgmt__summaryCard--warn"
>
<div class="device-mgmt__summaryValue">{{ alarmSummary.unresolved }}</div>
<div class="device-mgmt__summaryLabel">未处理</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="8">
<el-card
shadow="never"
class="device-mgmt__summaryCard device-mgmt__summaryCard--ok"
>
<div class="device-mgmt__summaryValue">{{ alarmSummary.resolved }}</div>
<div class="device-mgmt__summaryLabel">已处理</div>
</el-card>
</el-col>
</el-row>
<el-table :data="alarmList" class="mt-16px" border>
<el-table-column label="报警时间" prop="time" width="180" />
<el-table-column label="报警类型" prop="typeLabel" min-width="140" />
<el-table-column label="报警级别" prop="level" width="120">
<el-table-column label="报警时间" prop="createTime" width="180">
<template #default="{ row }">
{{ formatDate(row.createTime) }}
</template>
</el-table-column>
<el-table-column label="设备名称" prop="deviceName" min-width="140" />
<el-table-column label="模型名称" prop="modelName" min-width="120" />
<el-table-column label="规则名称" prop="ruleName" min-width="120" />
<el-table-column label="报警级别" prop="alarmLevel" width="120">
<template #default="{ row }">
<el-tag
:type="
row.level === '高' ? 'danger' : row.level === '中' ? 'warning' : 'info'
String(row.alarmLevel) === '1'
? 'danger'
: String(row.alarmLevel) === '2'
? 'warning'
: 'info'
"
>
{{ row.level }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="当前值" prop="current" width="120" />
<el-table-column label="阈值" prop="threshold" width="120" />
<el-table-column label="状态" prop="status" width="120">
<template #default="{ row }">
<el-tag :type="row.status === '已处理' ? 'success' : 'warning'">
{{ row.status }}
{{
String(row.alarmLevel) === '1'
? '高级'
: String(row.alarmLevel) === '2'
? '中级'
: '低级'
}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="当前值" prop="addressValue" width="120" />
</el-table>
<Pagination
v-model:page="alarmPage"
v-model:limit="alarmPageSize"
:total="alarmTotal"
@pagination="loadAlarmData"
class="mt-16px"
/>
</el-card>
</template>
</div>
@ -387,14 +496,16 @@
</template>
<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue'
import { computed, ref, watchEffect, watch } from 'vue'
import type { EChartsOption } from 'echarts'
import Echart from '@/components/Echart/src/Echart.vue'
import { DeviceApi } from '@/api/iot/device'
import { OrgNodeApi } from '@/api/iot/orgNode'
import { DeviceModelAttributeApi } from '@/api/iot/devicemodelattribute'
import { useRoute } from 'vue-router'
import NodeForm from './NodeForm.vue'
import DeviceForm from '../DeviceForm.vue'
import { formatDate } from '@/utils/formatTime'
defineOptions({ name: 'IoTDeviceManagement' })
@ -406,6 +517,8 @@ type HistoryRange = 'today' | 'week' | 'month'
type TreeNode = {
id: string
parentId?: string
parentKey?: string
nodeKey?: string
customerId?: string
nodeType?: number
deviceId?: string
@ -446,7 +559,7 @@ const message = useMessage()
const route = useRoute()
const treeData = ref<TreeNode[]>([])
const selectedNode = ref<TreeNode>()
const selectedDevice = ref<TreeNode>()
const selectedDeviceDetail = ref<Record<string, any>>({})
const customerId = ref<string | undefined>((route.query.customerId as string) || undefined)
const activeTab = ref<'base' | 'realtime' | 'history' | 'alarm'>('base')
const nodeFormRef = ref()
@ -492,6 +605,22 @@ const toNodeType = (nodeType?: number | string): TreeNodeType => {
return 'device'
}
const getNodeTypeLabel = (nodeType?: number | string) => {
const value = Number(nodeType)
if (value === 2) return '车间'
if (value === 3) return '产线'
if (value === 4) return '设备'
return '组织'
}
const getNodeTypeClass = (nodeType?: number | string) => {
const value = Number(nodeType)
if (value === 2) return 'device-mgmt__treeNodeType--workshop'
if (value === 3) return 'device-mgmt__treeNodeType--line'
if (value === 4) return 'device-mgmt__treeNodeType--device'
return 'device-mgmt__treeNodeType--default'
}
const toBooleanStatus = (raw: any): boolean => {
if (typeof raw.online === 'boolean') return raw.online
if (typeof raw.isOnline === 'boolean') return raw.isOnline
@ -505,6 +634,8 @@ const normalizeNode = (raw: any): TreeNode => {
return {
id,
parentId: raw.parentId !== undefined ? String(raw.parentId) : undefined,
parentKey: raw.parentKey !== undefined ? String(raw.parentKey) : undefined,
nodeKey: raw.nodeKey !== undefined ? String(raw.nodeKey) : undefined,
customerId: raw.customerId !== undefined ? String(raw.customerId) : undefined,
nodeType: Number(raw.nodeType ?? raw.type ?? 3),
deviceId: raw.deviceId !== undefined ? String(raw.deviceId) : undefined,
@ -516,28 +647,6 @@ const normalizeNode = (raw: any): TreeNode => {
}
}
const mapDeviceDetail = (raw: any): TreeNode => {
const status = raw.status ?? raw.isConnect ?? raw.online
const online = status === 1 || status === '1' || status === 'online' || status === '在线'
return {
id: String(raw.id ?? ''),
type: 'device',
name: raw.deviceName ?? '-',
code: raw.deviceCode ?? '-',
online,
model: raw.deviceModelName ?? raw.model ?? '-',
manufacturer: raw.manufacturer ?? raw.deviceBrandName ?? '-',
customerName: raw.customerName ?? '-',
lineName: raw.lineName ?? '-',
address: raw.address ?? raw.location ?? '-',
mqttTopic: raw.readTopic ?? '-',
mqttClientId: raw.clientId ?? raw.deviceCode ?? '-',
createdAt: raw.createTime ?? '-',
updatedAt: raw.updateTime ?? raw.lastOnlineTime ?? '-',
remark: raw.remark ?? '-'
}
}
const getCustomerId = (node?: TreeNode) => {
return node?.customerId || customerId.value || treeData.value[0]?.customerId || ''
}
@ -570,7 +679,7 @@ const getFirstSelectableNode = (nodes: TreeNode[]): TreeNode | undefined => {
const loadDeviceDetail = async (deviceId: number) => {
const detail = await DeviceApi.getDevice(deviceId)
selectedDevice.value = mapDeviceDetail(detail)
selectedDeviceDetail.value = detail || {}
}
const getOperatingStatusType = (status?: string | number) => {
@ -600,6 +709,15 @@ const loadNodeDeviceList = async () => {
}
}
const handleDeviceEnableChange = async (row: DeviceListRow, enabled: boolean) => {
try {
await DeviceApi.updateDeviceEnabled({ id: row.id, enabled })
message.success(enabled ? '启用成功' : '停用成功')
} catch (error) {
row.isEnable = !enabled
}
}
const handleTreeNodeClick = async (node: TreeNode) => {
selectedNode.value = node
rightMode.value = 'list'
@ -669,8 +787,17 @@ const handleNodeFormSuccess = async () => {
}
const removeNode = async (node: TreeNode) => {
if (Array.isArray(node.children) && node.children.length > 0) {
message.warning('当前节点存在子节点,无法删除')
return
}
await message.delConfirm(`确认删除节点「${node.name}」吗?`)
await OrgNodeApi.deleteOrgNode(node.id)
if (Number(node.nodeType) === 4) {
const deviceId = Number(node.deviceId || node.id)
await DeviceApi.deleteDevice(String(deviceId))
} else {
await OrgNodeApi.deleteOrgNode(node.id)
}
message.success('删除成功')
if (selectedNode.value?.id === node.id) {
selectedNode.value = undefined
@ -704,76 +831,109 @@ onMounted(async () => {
await getTreeData()
})
const buildRealtimeMetrics = (online?: boolean): Metric[] => {
const base: Metric[] = [
{ key: 'temp', name: '温度', value: 45.2, unit: '℃', status: 'ok' },
{ key: 'pressure', name: '压力', value: 0.85, unit: 'MPa', status: 'ok' },
{ key: 'power', name: '功率', value: 1200, unit: 'W', status: 'ok' },
{ key: 'current', name: '电流', value: 15.6, unit: 'A', status: 'warn' },
{ key: 'voltage', name: '电压', value: 380, unit: 'V', status: 'ok' },
{ key: 'flow', name: '水流', value: 8.5, unit: 'L/min', status: 'ok' },
{ key: 'runtime', name: '运行时间', value: 8.5, unit: 'h', status: 'ok' }
]
if (!online) {
return base.map((m) => ({ ...m, status: 'warn', value: Number((m.value * 0.6).toFixed(2)) }))
const realtimeList = ref<any[]>([])
const realtimeTotal = ref(0)
const realtimePage = ref(1)
const realtimePageSize = ref(16)
const realtimeLoading = ref(false)
const loadRealtimeData = async () => {
if (!selectedDeviceDetail.value?.id) return
realtimeLoading.value = true
try {
const res = await DeviceApi.getDeviceAttributePage({
pageNo: realtimePage.value,
pageSize: realtimePageSize.value,
deviceId: selectedDeviceDetail.value.id
})
realtimeList.value = res?.list || []
realtimeTotal.value = res?.total || 0
} finally {
realtimeLoading.value = false
}
return base
}
const realtimeMetrics = computed(() => buildRealtimeMetrics(selectedDevice.value?.online))
const lastRealtimeAt = computed(() =>
selectedDevice.value?.updatedAt ? selectedDevice.value.updatedAt : '-'
)
const historyMetricOptions = computed(() =>
realtimeMetrics.value
.filter((m) => m.key !== 'runtime')
.map((m) => ({ key: m.key, name: m.name }))
)
const historyMetricKey = ref<MetricKey>('temp')
const historyRange = ref<HistoryRange>('today')
watchEffect(() => {
if (
!historyMetricOptions.value.find((m) => m.key === historyMetricKey.value) &&
historyMetricOptions.value.length
) {
historyMetricKey.value = historyMetricOptions.value[0].key
watch([() => activeTab.value, () => selectedDeviceDetail.value.id], ([tab, deviceId]) => {
if (tab === 'realtime' && deviceId) {
realtimePage.value = 1
loadRealtimeData()
}
})
const buildHistory = (range: HistoryRange, base: number) => {
const points = range === 'today' ? 24 : range === 'week' ? 7 * 6 : 30 * 3
const labels: string[] = []
const values: number[] = []
for (let i = 0; i < points; i++) {
if (range === 'today') {
labels.push(String(i).padStart(2, '0') + ':00')
} else {
labels.push(`D${i + 1}`)
const historyMetricOptions = ref<any[]>([])
const historyMetricKey = ref<string | number>('')
const historyTimeRange = ref<string[]>([])
const historyViewType = ref<'chart' | 'list'>('chart')
const historyList = ref<any[]>([])
const historyLoading = ref(false)
const loadHistoryMetrics = async () => {
if (!selectedDeviceDetail.value?.id) return
try {
const list = await DeviceApi.getDeviceAttributeList(selectedDeviceDetail.value.id)
historyMetricOptions.value = Array.isArray(list) ? list : []
if (historyMetricOptions.value.length > 0 && !historyMetricKey.value) {
historyMetricKey.value =
historyMetricOptions.value[0].attributeCode || historyMetricOptions.value[0].attributeName
}
const noise = Math.sin(i / 2.3) * 0.9 + Math.cos(i / 5.2) * 0.6
const drift = (i / points) * 0.8
values.push(Number((base + noise + drift).toFixed(2)))
} catch (e) {
console.error(e)
}
return { labels, values }
}
const metricBaseValue = (key: MetricKey) => {
if (key === 'temp') return 42
if (key === 'pressure') return 0.8
if (key === 'power') return 1180
if (key === 'current') return 13
if (key === 'flow') return 8
if (key === 'voltage') return 380
if (key === 'runtime') return 8
return 1
const loadHistoryData = async () => {
if (!selectedDeviceDetail.value?.id) return
historyLoading.value = true
try {
const params: any = {
deviceId: selectedDeviceDetail.value.id,
modelId: selectedDeviceDetail.value.deviceModelId
}
if (historyTimeRange.value && historyTimeRange.value.length === 2) {
params.collectionStartTime = historyTimeRange.value[0]
params.collectionEndTime = historyTimeRange.value[1]
}
const res = await DeviceModelAttributeApi.operationAnalysisDetails(params)
let data = Array.isArray(res) ? res : res?.list || []
// Convert property names if necessary, assuming common names like createTime/time, addressValue/value, etc.
historyList.value = data.map((item: any) => ({
...item,
time: item.time || item.createTime || item.collectionTime,
value: item.value || item.addressValue || item.dataValue,
attributeName: item.attributeName || item.parameter || item.name,
unit: item.unit || item.dataUnit || ''
}))
} catch (e) {
console.error(e)
} finally {
historyLoading.value = false
}
}
const historyChartOptions = computed<EChartsOption>(() => {
const metric = realtimeMetrics.value.find((m) => m.key === historyMetricKey.value)
const base = metricBaseValue(historyMetricKey.value)
const { labels, values } = buildHistory(historyRange.value, base)
let data = historyList.value
if (historyMetricKey.value) {
data = data.filter(
(item) =>
item.attributeCode === historyMetricKey.value ||
item.attributeName === historyMetricKey.value
)
}
// Sort by time ascending for chart
const sortedData = [...data].sort(
(a, b) => new Date(a.time).getTime() - new Date(b.time).getTime()
)
const labels = sortedData.map((item) => formatDate(item.time))
const values = sortedData.map((item) => Number(item.value) || 0)
const metricName =
historyMetricOptions.value.find(
(m) =>
m.attributeCode === historyMetricKey.value || m.attributeName === historyMetricKey.value
)?.attributeName || '数据'
return {
grid: { left: 20, right: 20, bottom: 20, top: 40, containLabel: true },
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' }, padding: [6, 10] },
@ -786,7 +946,7 @@ const historyChartOptions = computed<EChartsOption>(() => {
yAxis: { type: 'value', axisTick: { show: false } },
series: [
{
name: metric?.name || '数据',
name: metricName,
type: 'line',
smooth: true,
showSymbol: false,
@ -797,88 +957,40 @@ const historyChartOptions = computed<EChartsOption>(() => {
}
})
type AlarmStatus = '已处理' | '未处理'
type AlarmLevel = '高' | '中' | '低'
type AlarmRow = {
id: string
time: string
type: string
typeLabel: string
level: AlarmLevel
current: string
threshold: string
status: AlarmStatus
}
const alarmType = ref<string | undefined>(undefined)
const alarmRange = ref<HistoryRange>('today')
const mockAlarmAll = computed<AlarmRow[]>(() => {
const base = selectedDevice.value?.code || 'DEV'
return [
{
id: `${base}-a1`,
time: '2026-03-10 10:20:00',
type: 'temp_high',
typeLabel: '温度过高',
level: '高',
current: '85.6℃',
threshold: '80℃',
status: '未处理'
},
{
id: `${base}-a2`,
time: '2026-03-10 09:15:00',
type: 'pressure_high',
typeLabel: '压力过高',
level: '中',
current: '1.2MPa',
threshold: '1.0MPa',
status: '已处理'
},
{
id: `${base}-a3`,
time: '2026-03-09 18:30:00',
type: 'current_high',
typeLabel: '电流过高',
level: '中',
current: '16.8A',
threshold: '15A',
status: '已处理'
},
{
id: `${base}-a4`,
time: '2026-03-09 14:22:00',
type: 'temp_high',
typeLabel: '温度过高',
level: '低',
current: '78.2℃',
threshold: '75℃',
status: '已处理'
},
{
id: `${base}-a5`,
time: '2026-03-08 11:45:00',
type: 'pressure_high',
typeLabel: '压力波动',
level: '低',
current: '-',
threshold: '-',
status: '已处理'
}
]
watch([() => activeTab.value, () => selectedDeviceDetail.value.id], ([tab, deviceId]) => {
if (tab === 'history' && deviceId) {
loadHistoryMetrics()
loadHistoryData()
}
})
const alarmList = computed(() => {
const list = mockAlarmAll.value
if (!alarmType.value) return list
return list.filter((x) => x.type === alarmType.value)
})
const alarmList = ref<any[]>([])
const alarmTotal = ref(0)
const alarmPage = ref(1)
const alarmPageSize = ref(10)
const alarmLoading = ref(false)
const alarmSummary = computed(() => {
const total = alarmList.value.length
const unresolved = alarmList.value.filter((x) => x.status === '未处理').length
return { total, unresolved, resolved: total - unresolved }
const loadAlarmData = async () => {
if (!selectedDeviceDetail.value?.id) return
alarmLoading.value = true
try {
const res = await DeviceApi.getDeviceWarinningRecordPage({
pageNo: alarmPage.value,
pageSize: alarmPageSize.value,
deviceId: selectedDeviceDetail.value.id
})
alarmList.value = res?.list || []
alarmTotal.value = res?.total || 0
} finally {
alarmLoading.value = false
}
}
watch([() => activeTab.value, () => selectedDeviceDetail.value.id], ([tab, deviceId]) => {
if (tab === 'alarm' && deviceId) {
alarmPage.value = 1
loadAlarmData()
}
})
</script>
@ -972,6 +1084,39 @@ const alarmSummary = computed(() => {
white-space: nowrap;
}
.device-mgmt__treeNodeType {
flex-shrink: 0;
padding: 0 6px;
line-height: 18px;
border-radius: 4px;
font-size: 12px;
border: 1px solid transparent;
}
.device-mgmt__treeNodeType--workshop {
color: var(--el-color-warning);
background: var(--el-color-warning-light-9);
border-color: var(--el-color-warning-light-7);
}
.device-mgmt__treeNodeType--line {
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
border-color: var(--el-color-primary-light-7);
}
.device-mgmt__treeNodeType--device {
color: var(--el-color-success);
background: var(--el-color-success-light-9);
border-color: var(--el-color-success-light-7);
}
.device-mgmt__treeNodeType--default {
color: var(--el-text-color-secondary);
background: var(--el-fill-color-light);
border-color: var(--el-border-color-light);
}
.device-mgmt__treeNodeMeta {
font-size: 12px;
color: var(--el-text-color-secondary);
@ -982,7 +1127,10 @@ const alarmSummary = computed(() => {
margin-left: auto;
display: none;
align-items: center;
gap: 4px;
.el-button {
margin: 0;
}
}
.device-mgmt__treeNode:hover .device-mgmt__treeNodeActions {

Loading…
Cancel
Save