feat:设备管理页面对接

master
黄伟杰 2 months ago
parent f84277c967
commit dc5cb0990f

@ -0,0 +1,50 @@
import request from '@/config/axios'
export interface OrgNodeVO {
id?: number | string
customerId?: number | string
parentId?: number | string
nodeType?: number | string
name?: string
sort?: number | string
createTime?: string
children?: OrgNodeVO[]
deviceId?: number | string
}
export interface OrgNodeQuery {
customerId?: number | string
}
export interface OrgNodeSaveReqVO {
id?: number | string
customerId?: number | string
parentId: number | string
nodeType: number | string
name: string
sort?: number | string
createTime?: string
}
export interface OrgNodeDeviceListReqVO {
nodeId: number | string
nodeType: number | string
}
export const OrgNodeApi = {
getOrgNodeTree: async (params?: OrgNodeQuery) => {
return await request.get<OrgNodeVO[]>({ url: '/iot/org-node/tree', params })
},
createOrgNode: async (data: OrgNodeSaveReqVO) => {
return await request.post({ url: '/iot/org-node/create', data })
},
updateOrgNode: async (data: OrgNodeSaveReqVO) => {
return await request.put({ url: '/iot/org-node/update', data })
},
getDeviceListByNode: async (params: OrgNodeDeviceListReqVO) => {
return await request.get({ url: '/iot/org-node/device-list-by-node', params })
},
deleteOrgNode: async (id: number | string) => {
return await request.delete({ url: '/iot/org-node/delete?id=' + id })
}
}

@ -732,7 +732,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true,
activeMenu: '/iot/device/device'
},
component: () => import('@/views/iot/device/index.vue')
component: () => import('@/views/iot/device/management/index.vue')
},
{
path: 'ota/operation/firmware/detail/:id',

@ -0,0 +1,319 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="680px" :fullscreen="false">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="110px"
v-loading="formLoading"
>
<el-form-item label="父组织" prop="parentId">
<el-tree-select
v-model="formData.parentId"
:data="parentTreeOptions"
:props="treeProps"
node-key="id"
check-strictly
clearable
filterable
:render-after-expand="false"
placeholder="请选择父组织"
class="!w-full"
/>
</el-form-item>
<el-form-item label="节点类型" prop="nodeType">
<el-radio-group v-model="formData.nodeType">
<el-radio v-for="item in currentNodeTypeOptions" :key="item.value" :label="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<template v-if="formData.nodeType === 4">
<el-form-item label="设备编号" prop="deviceCode">
<el-input v-model="formData.deviceCode" :disabled="formType === 'update'" />
</el-form-item>
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="formData.deviceName" />
</el-form-item>
<el-form-item label="设备模型" prop="deviceModelId">
<el-select
v-model="formData.deviceModelId"
clearable
filterable
:disabled="formType === 'update'"
placeholder="请选择设备模型"
class="!w-full"
>
<el-option
v-for="item in modelList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</template>
<template v-else>
<el-form-item label="节点名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入节点名称" />
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="formLoading" @click="submitForm"></el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DeviceApi } from '@/api/iot/device'
import { DeviceModelApi, DeviceModelVO } from '@/api/iot/devicemodel'
import { OrgNodeApi } from '@/api/iot/orgNode'
import { getIntDictOptions } from '@/utils/dict'
type TreeNode = {
id: string
deviceId?: string
parentId?: string
customerId?: string
nodeType?: number
name: string
children?: TreeNode[]
}
defineOptions({ name: 'IoTDeviceManagementNodeForm' })
const props = defineProps<{
treeData: TreeNode[]
customerId?: string
}>()
const emit = defineEmits<{
success: []
}>()
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const formLoading = ref(false)
const formType = ref<'create' | 'update'>('create')
const dialogTitle = computed(() =>
formType.value === 'create' ? t('action.create') : t('action.update')
)
const formRef = ref()
const modelList = ref<DeviceModelVO[]>([])
const treeProps = { children: 'children', label: 'name', value: 'id' }
const currentParentNode = ref<TreeNode>()
const formData = ref({
id: undefined as number | undefined,
customerId: undefined as number | undefined,
parentId: undefined as string | undefined,
nodeType: 2,
name: '',
deviceCode: '',
deviceName: '',
deviceModelId: undefined as number | undefined
})
const nodeTypeOptions = computed(() => {
const list = getIntDictOptions('iot_node_type').filter((item) => [2, 3, 4].includes(item.value))
if (list.length) return list
return [
{ label: '车间', value: 2 },
{ label: '产线', value: 3 },
{ label: '设备', value: 4 }
]
})
const currentNodeTypeOptions = computed(() => {
const parentType = Number(currentParentNode.value?.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))
})
const parentTreeOptions = computed(() => {
const filterNode = (nodes: TreeNode[]): TreeNode[] => {
return nodes
.filter((node) => Number(node.nodeType) !== 4)
.map((node) => ({
...node,
children: node.children?.length ? filterNode(node.children) : []
}))
}
return filterNode(props.treeData || [])
})
const formRules = computed(() => {
const common = {
parentId: [{ required: true, message: '父组织不能为空', trigger: 'change' }],
nodeType: [{ required: true, message: '节点类型不能为空', trigger: 'change' }]
}
if (formData.value.nodeType === 4) {
return {
...common,
deviceCode: [{ required: true, message: '设备编号不能为空', trigger: 'blur' }],
deviceName: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }],
deviceModelId: [{ required: true, message: '设备模型不能为空', trigger: 'change' }]
}
}
return {
...common,
name: [{ required: true, message: '节点名称不能为空', trigger: 'blur' }]
}
})
const getDefaultNodeType = (parent?: TreeNode) => {
const parentType = Number(parent?.nodeType || 1)
if (parentType === 2) return 3
if (parentType === 3) return 4
return 2
}
const loadModels = async () => {
if (modelList.value.length) return
const data = await DeviceModelApi.getDeviceModelList()
modelList.value = Array.isArray(data) ? data : []
}
watch(
() => formData.value.nodeType,
async (nodeType) => {
if (nodeType === 4) {
await loadModels()
}
}
)
const findNodeById = (id?: string): TreeNode | undefined => {
if (!id) return undefined
const stack = [...(props.treeData || [])]
while (stack.length) {
const node = stack.shift()!
if (node.id === id) return node
if (node.children?.length) stack.push(...node.children)
}
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
}
let current = node
while (current) {
if (Number(current.nodeType) === 1) {
return Number(current.customerId || current.id || props.customerId || '') || undefined
}
current = findNodeById(current.parentId)
}
return Number(props.customerId || '') || undefined
}
const open = async (type: 'create' | 'update', node?: TreeNode, defaultParentId?: string) => {
formType.value = type
formRef.value?.resetFields()
if (type === 'create') {
currentParentNode.value = node
const nodeType = getDefaultNodeType(node)
const initialParentId = Number(node?.nodeType) === 1 ? '0' : defaultParentId || node?.id
formData.value = {
id: undefined,
customerId: getCustomerIdByNode(node),
parentId: initialParentId,
nodeType,
name: '',
deviceCode: '',
deviceName: '',
deviceModelId: undefined
}
if (
currentNodeTypeOptions.value.length &&
!currentNodeTypeOptions.value.find((item) => item.value === formData.value.nodeType)
) {
formData.value.nodeType = currentNodeTypeOptions.value[0].value
}
} else {
currentParentNode.value = findNodeById(node?.parentId)
const deviceId =
Number(node?.deviceId || '') ||
(Number(node?.nodeType) === 4 ? Number(node?.id || '') : undefined)
formData.value = {
id: deviceId || Number(node?.id),
customerId: getCustomerIdByNode(currentParentNode.value || node),
parentId: node?.parentId,
nodeType: Number(node?.nodeType || 2),
name: node?.name || '',
deviceCode: '',
deviceName: '',
deviceModelId: undefined
}
if (Number(node?.nodeType) === 4 && deviceId) {
const device = await DeviceApi.getDevice(deviceId)
formData.value.deviceCode = device?.deviceCode || ''
formData.value.deviceName = device?.deviceName || ''
formData.value.deviceModelId = device?.deviceModelId
formData.value.customerId =
Number(device?.customerId || formData.value.customerId || props.customerId || '') ||
undefined
}
}
if (formData.value.nodeType === 4) {
await loadModels()
}
dialogVisible.value = true
}
defineExpose({ open })
const submitForm = async () => {
await formRef.value.validate()
if (!formData.value.customerId) {
message.warning('未识别到客户ID请检查树节点数据')
return
}
const parentId =
formType.value === 'create' && Number(currentParentNode.value?.nodeType) === 1
? 0
: Number(formData.value.parentId)
const payload = {
customerId: Number(formData.value.customerId),
parentId,
nodeType: Number(formData.value.nodeType),
name: formData.value.name?.trim()
}
formLoading.value = true
try {
if (formData.value.nodeType === 4) {
const devicePayload: any = {
customerId: payload.customerId,
parentId: payload.parentId,
nodeType: payload.nodeType,
deviceCode: formData.value.deviceCode?.trim(),
deviceName: formData.value.deviceName?.trim(),
deviceModelId: formData.value.deviceModelId,
isEnable: false
}
if (formType.value === 'update' && formData.value.id) {
devicePayload.id = formData.value.id
await DeviceApi.updateDevice(devicePayload)
message.success(t('common.updateSuccess'))
} else {
await DeviceApi.createDevice(devicePayload)
message.success(t('common.createSuccess'))
}
} else if (formType.value === 'create') {
await OrgNodeApi.createOrgNode(payload as any)
message.success(t('common.createSuccess'))
} else {
await OrgNodeApi.updateOrgNode({ ...(payload as any), id: formData.value.id })
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
</script>

@ -10,24 +10,6 @@
placeholder="搜索设备/产线/公司"
class="device-mgmt__search"
/>
<div class="device-mgmt__stats">
<div class="device-mgmt__stat">
<div class="device-mgmt__statLabel">设备</div>
<div class="device-mgmt__statValue">{{ deviceTotal }}</div>
</div>
<div class="device-mgmt__stat">
<div class="device-mgmt__statLabel">在线</div>
<div class="device-mgmt__statValue device-mgmt__statValue--ok">{{
deviceOnline
}}</div>
</div>
<div class="device-mgmt__stat">
<div class="device-mgmt__statLabel">离线</div>
<div class="device-mgmt__statValue device-mgmt__statValue--muted">
{{ deviceTotal - deviceOnline }}
</div>
</div>
</div>
</div>
<el-tree
@ -55,6 +37,31 @@
<template v-if="data.type === 'device'">
<span class="device-mgmt__treeNodeMeta">{{ data.code }}</span>
</template>
<div class="device-mgmt__treeNodeActions">
<el-button
v-if="canCreateChild(data)"
link
type="primary"
@click.stop="openCreateNodeForm(data)"
>
新增
</el-button>
<el-button
v-if="Number(data.nodeType) !== 1"
link
type="warning"
@click.stop="openUpdateNodeForm(data)"
>编辑</el-button
>
<el-button
v-if="Number(data.nodeType) !== 1"
link
type="danger"
@click.stop="removeNode(data)"
>
删除
</el-button>
</div>
</div>
</template>
</el-tree>
@ -62,12 +69,61 @@
</div>
<div class="device-mgmt__right">
<div class="device-mgmt__content" v-if="rightMode === 'list'">
<el-card shadow="never" class="device-mgmt__panel">
<template #header>
<div class="device-mgmt__listHeader">
<div class="device-mgmt__listTitle">
{{
selectedNode
? `${selectedNode.name}${Number(selectedNode.nodeType) === 4 ? '设备节点' : '组织节点'}`
: '设备列表'
}}
</div>
<!-- <div class="device-mgmt__listActions">
<el-button @click="loadNodeDeviceList"></el-button>
</div> -->
</div>
</template>
<el-table v-loading="nodeDeviceLoading" :data="nodeDeviceList" row-key="id" border>
<el-table-column label="设备编码" prop="deviceCode" min-width="160" />
<el-table-column label="设备名称" prop="deviceName" min-width="180" />
<el-table-column label="运行状态" prop="operatingStatus" width="120">
<template #default="{ row }">
<el-tag :type="getOperatingStatusType(row.operatingStatus)">
{{ getOperatingStatusLabel(row.operatingStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="协议" prop="protocol" width="120" />
<el-table-column label="启用" prop="isEnable" width="90">
<template #default="{ row }">
{{ row.isEnable ? '是' : '否' }}
</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)"
>设备设置</el-button
>
<el-button link type="primary" @click="openDeviceDetail(row)"></el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<template v-else>
<el-card shadow="never" class="device-mgmt__panel device-mgmt__header">
<div class="device-mgmt__headerMain">
<div class="device-mgmt__titleRow">
<el-button link type="primary" @click="backToList"></el-button>
<span
class="device-mgmt__dot device-mgmt__dot--lg"
:class="selectedDevice?.online ? 'device-mgmt__dot--ok' : 'device-mgmt__dot--muted'"
:class="
selectedDevice?.online ? 'device-mgmt__dot--ok' : 'device-mgmt__dot--muted'
"
></span>
<div class="device-mgmt__titleText">
<div class="device-mgmt__title">{{ selectedDevice?.name || '-' }}</div>
@ -117,7 +173,9 @@
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">所属产线</div>
<div class="device-mgmt__baseValue">{{ selectedDevice?.lineName || '-' }}</div>
<div class="device-mgmt__baseValue">{{
selectedDevice?.lineName || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">安装位置</div>
@ -160,11 +218,15 @@
<div class="device-mgmt__baseGrid">
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">创建时间</div>
<div class="device-mgmt__baseValue">{{ selectedDevice?.createdAt || '-' }}</div>
<div class="device-mgmt__baseValue">{{
selectedDevice?.createdAt || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">最后更新</div>
<div class="device-mgmt__baseValue">{{ selectedDevice?.updatedAt || '-' }}</div>
<div class="device-mgmt__baseValue">{{
selectedDevice?.updatedAt || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem device-mgmt__baseItem--full">
<div class="device-mgmt__baseLabel">备注</div>
@ -206,7 +268,11 @@
<div class="device-mgmt__filter">
<div class="device-mgmt__filterLabel">参数选择</div>
<el-radio-group v-model="historyMetricKey" size="small">
<el-radio-button v-for="m in historyMetricOptions" :key="m.key" :label="m.key">
<el-radio-button
v-for="m in historyMetricOptions"
:key="m.key"
:label="m.key"
>
{{ m.name }}
</el-radio-button>
</el-radio-group>
@ -307,15 +373,28 @@
</el-card>
</template>
</div>
</template>
</div>
</div>
</ContentWrap>
<NodeForm
ref="nodeFormRef"
:tree-data="treeData"
:customer-id="customerId"
@success="handleNodeFormSuccess"
/>
<DeviceForm ref="deviceFormRef" @success="onDeviceFormSuccess" />
</template>
<script setup lang="ts">
import { computed, ref, watchEffect } 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 { useRoute } from 'vue-router'
import NodeForm from './NodeForm.vue'
import DeviceForm from '../DeviceForm.vue'
defineOptions({ name: 'IoTDeviceManagement' })
@ -326,6 +405,10 @@ type HistoryRange = 'today' | 'week' | 'month'
type TreeNode = {
id: string
parentId?: string
customerId?: string
nodeType?: number
deviceId?: string
type: TreeNodeType
name: string
code?: string
@ -346,128 +429,31 @@ type TreeNode = {
}
type Metric = { key: MetricKey; name: string; value: number; unit: string; status: MetricStatus }
type DeviceListRow = {
id: number
deviceCode?: string
deviceName?: string
operatingStatus?: string | number
protocol?: string
isEnable?: boolean
collectionTime?: string
}
const treeProps = { children: 'children', label: 'name' }
const treeRef = ref()
const treeKeyword = ref('')
const treeData = ref<TreeNode[]>([
{
id: 'c1',
type: 'company',
name: '北京科技有限公司',
children: [
{
id: 'c1-l1',
type: 'line',
name: 'SMT产线A',
children: [
{
id: 'd-1001',
type: 'device',
name: '贴片机#1',
code: 'BJ-PL01-DEV01',
online: true,
companyName: '北京科技有限公司',
customerName: '北京科技有限公司',
lineName: 'SMT产线A',
model: 'SM471',
manufacturer: '三星',
address: '车间A区-01工位',
mqttTopic: 'device/bj/pl01/dev01',
mqttClientId: 'client_bj_dev01',
createdAt: '2026-01-15 09:00:00',
updatedAt: '2026-03-10 10:30:00',
remark: '位于车间A区主贴片工位'
},
{
id: 'd-1002',
type: 'device',
name: '注塑机#2',
code: 'SH-P01-DEV02',
online: false,
companyName: '北京科技有限公司',
customerName: '北京科技有限公司',
lineName: 'SMT产线A',
model: 'MA3200',
productKey: 'INJ-3200',
address: '一号车间 02 工位',
mqttTopic: 'dev/iot/sh_p01/dev02',
mqttClientId: 'client_sh_p01_dev02',
createdAt: '2026-02-02 09:20:00',
updatedAt: '2026-03-09 18:30:00',
remark: '离线待检修'
}
]
}
]
},
{
id: 'c2',
type: 'company',
name: '上海智联科技股份公司',
children: [
{
id: 'c2-l1',
type: 'line',
name: '装配产线',
children: [
{
id: 'd-2001',
type: 'device',
name: '装配线#1',
code: 'SH-A01-DEV01',
online: true,
companyName: '上海智联科技股份公司',
lineName: '装配产线',
model: 'ASSY-X1',
productKey: 'ASSY-X1',
address: '二号车间 A 区',
mqttTopic: 'dev/iot/sh_a01/dev01',
mqttClientId: 'client_sh_a01_dev01',
createdAt: '2025-12-01 12:00:00',
updatedAt: '2026-03-10 10:28:10',
remark: '装配主线设备'
}
]
}
]
},
{
id: 'c3',
type: 'company',
name: '杭州云计算中心',
children: [
{
id: 'c3-l1',
type: 'line',
name: '测试产线',
children: [
{
id: 'd-3001',
type: 'device',
name: '测试机#1',
code: 'HZ-T01-DEV01',
online: true,
companyName: '杭州云计算中心',
lineName: '测试产线',
model: 'TEST-PRO',
productKey: 'TEST-PRO',
address: '三号车间 03 工位',
mqttTopic: 'dev/iot/hz_t01/dev01',
mqttClientId: 'client_hz_t01_dev01',
createdAt: '2026-01-02 11:10:00',
updatedAt: '2026-03-10 10:15:00',
remark: '用于产线测试'
}
]
}
]
}
])
const selectedDeviceId = ref<string>('')
const message = useMessage()
const route = useRoute()
const treeData = ref<TreeNode[]>([])
const selectedNode = ref<TreeNode>()
const selectedDevice = ref<TreeNode>()
const customerId = ref<string | undefined>((route.query.customerId as string) || undefined)
const activeTab = ref<'base' | 'realtime' | 'history' | 'alarm'>('base')
const nodeFormRef = ref()
const deviceFormRef = ref()
const rightMode = ref<'list' | 'detail'>('list')
const nodeDeviceLoading = ref(false)
const nodeDeviceList = ref<DeviceListRow[]>([])
const flattenDevices = (nodes: TreeNode[]) => {
const devices: TreeNode[] = []
@ -485,24 +471,6 @@ const deviceList = computed(() => flattenDevices(treeData.value))
const deviceTotal = computed(() => deviceList.value.length)
const deviceOnline = computed(() => deviceList.value.filter((d) => !!d.online).length)
const selectedDevice = computed(() => deviceList.value.find((d) => d.id === selectedDeviceId.value))
const getFirstDeviceId = (node: TreeNode): string | undefined => {
if (node.type === 'device') return node.id
const stack = [...(node.children || [])]
while (stack.length) {
const cur = stack.shift()!
if (cur.type === 'device') return cur.id
if (cur.children?.length) stack.unshift(...cur.children)
}
return undefined
}
const handleTreeNodeClick = (node: TreeNode) => {
const nextId = getFirstDeviceId(node)
if (nextId) selectedDeviceId.value = nextId
}
const filterTreeNode = (value: string, data: TreeNode) => {
if (!value) return true
const v = value.trim()
@ -517,10 +485,223 @@ watchEffect(() => {
treeRef.value?.filter?.(treeKeyword.value)
})
watchEffect(() => {
if (!selectedDeviceId.value && deviceList.value.length) {
selectedDeviceId.value = deviceList.value[0].id
const toNodeType = (nodeType?: number | string): TreeNodeType => {
const value = Number(nodeType)
if (value === 1) return 'company'
if (value === 2) return 'line'
return 'device'
}
const toBooleanStatus = (raw: any): boolean => {
if (typeof raw.online === 'boolean') return raw.online
if (typeof raw.isOnline === 'boolean') return raw.isOnline
if (typeof raw.status === 'number') return raw.status === 1
if (typeof raw.status === 'string') return ['online', '1', 'true', '在线'].includes(raw.status)
return false
}
const normalizeNode = (raw: any): TreeNode => {
const id = String(raw.id ?? raw.nodeId ?? '')
return {
id,
parentId: raw.parentId !== undefined ? String(raw.parentId) : undefined,
customerId: raw.customerId !== undefined ? String(raw.customerId) : undefined,
nodeType: Number(raw.nodeType ?? raw.type ?? 3),
deviceId: raw.deviceId !== undefined ? String(raw.deviceId) : undefined,
type: toNodeType(raw.nodeType ?? raw.type),
name: raw.name ?? raw.nodeName ?? '-',
code: raw.code ?? raw.deviceCode,
online: toBooleanStatus(raw),
children: Array.isArray(raw.children) ? raw.children.map((child) => normalizeNode(child)) : []
}
}
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 || ''
}
const getNodeDeviceId = (node: TreeNode): number | undefined => {
const direct = Number(node.deviceId)
if (Number.isFinite(direct) && direct > 0) return direct
if (node.type === 'device') {
const selfId = Number(node.id)
if (Number.isFinite(selfId) && selfId > 0) return selfId
}
if (!node.children?.length) return undefined
for (const child of node.children) {
const childId = getNodeDeviceId(child)
if (childId) return childId
}
return undefined
}
const getFirstSelectableNode = (nodes: TreeNode[]): TreeNode | undefined => {
for (const node of nodes) {
if (getNodeDeviceId(node)) return node
if (node.children?.length) {
const child = getFirstSelectableNode(node.children)
if (child) return child
}
}
return nodes[0]
}
const loadDeviceDetail = async (deviceId: number) => {
const detail = await DeviceApi.getDevice(deviceId)
selectedDevice.value = mapDeviceDetail(detail)
}
const getOperatingStatusType = (status?: string | number) => {
const statusText = String(status ?? '')
if (statusText === '运行' || statusText === 'running' || statusText === '1') return 'success'
if (statusText === '故障中' || statusText === 'fault') return 'danger'
if (statusText === '报警中' || statusText === 'alarm') return 'warning'
return 'info'
}
const getOperatingStatusLabel = (status?: string | number) => {
if (status === undefined || status === null || status === '') return '-'
return String(status)
}
const loadNodeDeviceList = async () => {
if (!selectedNode.value) return
nodeDeviceLoading.value = true
try {
const data = await OrgNodeApi.getDeviceListByNode({
nodeId: Number(selectedNode.value.id),
nodeType: Number(selectedNode.value.nodeType || 4)
})
nodeDeviceList.value = Array.isArray(data) ? data : []
} finally {
nodeDeviceLoading.value = false
}
}
const handleTreeNodeClick = async (node: TreeNode) => {
selectedNode.value = node
rightMode.value = 'list'
await loadNodeDeviceList()
}
const getTreeData = async () => {
const params = getCustomerId() ? { customerId: getCustomerId() } : {}
const data = await OrgNodeApi.getOrgNodeTree(params)
treeData.value = Array.isArray(data) ? data.map((item) => normalizeNode(item)) : []
if (!selectedNode.value) {
const firstNode = getFirstSelectableNode(treeData.value)
if (firstNode) await handleTreeNodeClick(firstNode)
return
}
const stack = [...treeData.value]
while (stack.length) {
const node = stack.shift()!
if (node.id === selectedNode.value.id) {
selectedNode.value = node
await loadNodeDeviceList()
break
}
if (node.children?.length) stack.push(...node.children)
}
}
const getDefaultParentNode = () => {
if (selectedNode.value && Number(selectedNode.value.nodeType) !== 4) return selectedNode.value
const device =
selectedNode.value && Number(selectedNode.value.nodeType) === 4 ? selectedNode.value : undefined
if (device?.parentId) {
const stack = [...treeData.value]
while (stack.length) {
const node = stack.shift()!
if (node.id === device.parentId) return node
if (node.children?.length) stack.push(...node.children)
}
}
return undefined
}
const canCreateChild = (node: TreeNode) => {
const nodeType = Number(node.nodeType)
return nodeType === 1 || nodeType === 2 || nodeType === 3
}
const openCreateNodeForm = (node?: TreeNode) => {
const parentNode = node || getDefaultParentNode()
if (!parentNode) {
message.warning('请先在左侧选择父组织后再新增子节点')
return
}
if (!canCreateChild(parentNode)) {
message.warning('当前节点不支持新增子节点')
return
}
nodeFormRef.value?.open('create', parentNode, parentNode.id)
}
const openUpdateNodeForm = (node: TreeNode) => {
nodeFormRef.value?.open('update', node, node.parentId)
}
const handleNodeFormSuccess = async () => {
await getTreeData()
}
const removeNode = async (node: TreeNode) => {
await message.delConfirm(`确认删除节点「${node.name}」吗?`)
await OrgNodeApi.deleteOrgNode(node.id)
message.success('删除成功')
if (selectedNode.value?.id === node.id) {
selectedNode.value = undefined
selectedDevice.value = undefined
}
await getTreeData()
}
const openForm = (type: string, id?: number) => {
deviceFormRef.value?.open(type, id)
}
const openDeviceDetail = async (row: DeviceListRow) => {
if (!row.id) return
await loadDeviceDetail(row.id)
rightMode.value = 'detail'
}
const backToList = () => {
rightMode.value = 'list'
}
const onDeviceFormSuccess = async () => {
await loadNodeDeviceList()
if (rightMode.value === 'detail' && selectedDevice.value?.id) {
await loadDeviceDetail(Number(selectedDevice.value.id))
}
}
onMounted(async () => {
await getTreeData()
})
const buildRealtimeMetrics = (online?: boolean): Metric[] => {
@ -733,6 +914,11 @@ const alarmSummary = computed(() => {
gap: 10px;
}
.device-mgmt__treeToolbar {
display: flex;
gap: 10px;
}
.device-mgmt__stat {
padding: 10px 12px;
border-radius: 8px;
@ -769,7 +955,7 @@ const alarmSummary = computed(() => {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
justify-content: flex-start;
gap: 12px;
}
@ -792,6 +978,17 @@ const alarmSummary = computed(() => {
white-space: nowrap;
}
.device-mgmt__treeNodeActions {
margin-left: auto;
display: none;
align-items: center;
gap: 4px;
}
.device-mgmt__treeNode:hover .device-mgmt__treeNodeActions {
display: flex;
}
.device-mgmt__dot {
width: 8px;
height: 8px;

Loading…
Cancel
Save