动作配置参数dialog弹窗修复+状态监控界面
This commit is contained in:
@@ -115,6 +115,11 @@ const router = createRouter({
|
|||||||
component: () => import('../views/recordinfo/index.vue'),
|
component: () => import('../views/recordinfo/index.vue'),
|
||||||
meta: { permission: 'tb:exceptionrecord' },
|
meta: { permission: 'tb:exceptionrecord' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/status-monitor',
|
||||||
|
name: 'status-monitor',
|
||||||
|
component: () => import('../views/statusmonitor/index.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -103,6 +103,10 @@
|
|||||||
<el-icon><component :is="getMenuIcon('进样控制')" /></el-icon>
|
<el-icon><component :is="getMenuIcon('进样控制')" /></el-icon>
|
||||||
<span>进样控制</span>
|
<span>进样控制</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/status-monitor">
|
||||||
|
<el-icon><component :is="getMenuIcon('监控')" /></el-icon>
|
||||||
|
<span>状态监控界面</span>
|
||||||
|
</el-menu-item>
|
||||||
<el-menu-item v-if="hasPermission('tb:goodrecord')" index="/goods-info">
|
<el-menu-item v-if="hasPermission('tb:goodrecord')" index="/goods-info">
|
||||||
<el-icon><component :is="getMenuIcon('样品管理')" /></el-icon>
|
<el-icon><component :is="getMenuIcon('样品管理')" /></el-icon>
|
||||||
<span>样品记录</span>
|
<span>样品记录</span>
|
||||||
@@ -182,6 +186,7 @@ const getMenuIcon = (menuName: string) => {
|
|||||||
'标准流程管理': Files,
|
'标准流程管理': Files,
|
||||||
'流程创建': EditPen,
|
'流程创建': EditPen,
|
||||||
进样控制: Operation,
|
进样控制: Operation,
|
||||||
|
监控: Monitor,
|
||||||
}
|
}
|
||||||
return iconMap[menuName] || EditPen
|
return iconMap[menuName] || EditPen
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,10 @@
|
|||||||
clearable
|
clearable
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
>
|
>
|
||||||
<el-option label="空闲" :value="0" />
|
<el-option label="离线" :value="0" />
|
||||||
<el-option label="运行" :value="1" />
|
<el-option label="空闲" :value="1" />
|
||||||
<el-option label="故障" :value="4" />
|
<el-option label="忙碌" :value="2" />
|
||||||
|
<el-option label="故障" :value="5" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="所属功能岛">
|
<el-form-item label="所属功能岛">
|
||||||
@@ -376,19 +377,31 @@ const getIndex = (index: number) => {
|
|||||||
return (pagination.pageNum - 1) * pagination.pageSize + index + 1
|
return (pagination.pageNum - 1) * pagination.pageSize + index + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeStatus = (value: any) => {
|
||||||
|
if (value === 0 || value === '0' || value === 'offline' || value === 'OFFLINE' || value === '离线') return 0
|
||||||
|
if (value === 1 || value === '1' || value === 'idle' || value === 'IDLE' || value === '空闲') return 1
|
||||||
|
if (value === 2 || value === '2' || value === 'busy' || value === 'BUSY' || value === '忙碌') return 2
|
||||||
|
if (value === 5 || value === '5' || value === 'fault' || value === 'FAULT' || value === '故障') return 5
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// 获取状态类型
|
// 获取状态类型
|
||||||
const getStatusType = (status: number | null | undefined) => {
|
const getStatusType = (status: number | string | null | undefined) => {
|
||||||
if (status === 0) return 'success' // 空闲
|
const normalized = normalizeStatus(status)
|
||||||
if (status === 1) return 'warning' // 运行
|
if (normalized === 0) return 'info'
|
||||||
if (status === 4) return 'danger' // 故障
|
if (normalized === 1) return 'success'
|
||||||
|
if (normalized === 2) return 'warning'
|
||||||
|
if (normalized === 5) return 'danger'
|
||||||
return 'info'
|
return 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取状态文本
|
// 获取状态文本
|
||||||
const getStatusText = (status: number | null | undefined) => {
|
const getStatusText = (status: number | string | null | undefined) => {
|
||||||
if (status === 0) return '空闲'
|
const normalized = normalizeStatus(status)
|
||||||
if (status === 1) return '运行'
|
if (normalized === 0) return '离线'
|
||||||
if (status === 4) return '故障'
|
if (normalized === 1) return '空闲'
|
||||||
|
if (normalized === 2) return '忙碌'
|
||||||
|
if (normalized === 5) return '故障'
|
||||||
return '未知'
|
return '未知'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -736,6 +749,9 @@ const currentDevice = ref<any>(null)
|
|||||||
const paramList = ref<any[]>([]) // 参数列表(分页)
|
const paramList = ref<any[]>([]) // 参数列表(分页)
|
||||||
const paramTotal = ref(0) // 参数总数
|
const paramTotal = ref(0) // 参数总数
|
||||||
const boundParamIds = ref<Set<number>>(new Set()) // 已绑定的参数ID集合
|
const boundParamIds = ref<Set<number>>(new Set()) // 已绑定的参数ID集合
|
||||||
|
const selectedParamIds = ref<Set<number>>(new Set()) // 当前配置中已勾选的参数ID集合(跨页保持)
|
||||||
|
const paramSelectionCache = ref<Record<string, number[]>>({}) // 参数勾选缓存(用于分页回显)
|
||||||
|
const syncingParamSelection = ref(false) // 是否正在程序化同步勾选状态
|
||||||
const paramConfigTableRef = ref<any>(null) // 参数表格引用
|
const paramConfigTableRef = ref<any>(null) // 参数表格引用
|
||||||
const categoryMap = ref<Record<number, string>>({}) // 参数分类ID到名称的映射
|
const categoryMap = ref<Record<number, string>>({}) // 参数分类ID到名称的映射
|
||||||
const categoryDicTypeId = ref<number | null>(null) // 参数分类的字典类型ID
|
const categoryDicTypeId = ref<number | null>(null) // 参数分类的字典类型ID
|
||||||
@@ -783,12 +799,34 @@ const getRowDevIds = (row: any): Set<string> => {
|
|||||||
return parseDevIdsField(row?.devIds ?? row?.dev_ids ?? row?.devIDS ?? row?.devIdsStr)
|
return parseDevIdsField(row?.devIds ?? row?.dev_ids ?? row?.devIDS ?? row?.devIdsStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开参数配置对话框
|
||||||
|
const getParamSelectionCacheKey = () => {
|
||||||
|
return currentDevice.value?.id ? String(currentDevice.value.id) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncParamSelectionCache = () => {
|
||||||
|
const cacheKey = getParamSelectionCacheKey()
|
||||||
|
if (!cacheKey) return
|
||||||
|
paramSelectionCache.value[cacheKey] = Array.from(selectedParamIds.value)
|
||||||
|
}
|
||||||
|
|
||||||
// 打开参数配置对话框
|
// 打开参数配置对话框
|
||||||
const handleConfigParams = async (row: any) => {
|
const handleConfigParams = async (row: any) => {
|
||||||
currentDevice.value = row
|
currentDevice.value = row
|
||||||
paramConfigDialogVisible.value = true
|
paramConfigDialogVisible.value = true
|
||||||
|
selectedParamIds.value = new Set()
|
||||||
await loadCategoryList()
|
await loadCategoryList()
|
||||||
await loadBoundParams()
|
await loadBoundParams()
|
||||||
|
|
||||||
|
const cacheKey = getParamSelectionCacheKey()
|
||||||
|
const cacheSelection = cacheKey ? paramSelectionCache.value[cacheKey] : undefined
|
||||||
|
if (cacheSelection?.length) {
|
||||||
|
selectedParamIds.value = new Set(cacheSelection.map((id) => Number(id)))
|
||||||
|
} else {
|
||||||
|
selectedParamIds.value = new Set(boundParamIds.value)
|
||||||
|
syncParamSelectionCache()
|
||||||
|
}
|
||||||
|
|
||||||
await loadParamList()
|
await loadParamList()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
setSelectedParams()
|
setSelectedParams()
|
||||||
@@ -953,18 +991,41 @@ const getParamCategoryName = (dicDataId: number | string | undefined | null) =>
|
|||||||
const setSelectedParams = () => {
|
const setSelectedParams = () => {
|
||||||
if (!paramConfigTableRef.value) return
|
if (!paramConfigTableRef.value) return
|
||||||
|
|
||||||
|
syncingParamSelection.value = true
|
||||||
paramConfigTableRef.value.clearSelection()
|
paramConfigTableRef.value.clearSelection()
|
||||||
|
|
||||||
paramList.value.forEach((param: any) => {
|
paramList.value.forEach((param: any) => {
|
||||||
if (boundParamIds.value.has(param.id)) {
|
if (selectedParamIds.value.has(Number(param.id))) {
|
||||||
paramConfigTableRef.value.toggleRowSelection(param, true)
|
paramConfigTableRef.value.toggleRowSelection(param, true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
syncingParamSelection.value = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 参数选择变化
|
// 参数选择变化
|
||||||
const handleParamSelectionChange = (_selection: any[]) => {
|
const handleParamSelectionChange = (selection: any[]) => {
|
||||||
// noop
|
if (syncingParamSelection.value) return
|
||||||
|
|
||||||
|
const currentPageSelectedIds = new Set<number>(
|
||||||
|
selection
|
||||||
|
.map((item: any) => item.id)
|
||||||
|
.filter((id: any) => id !== null && id !== undefined)
|
||||||
|
.map((id: any) => Number(id)),
|
||||||
|
)
|
||||||
|
|
||||||
|
paramList.value.forEach((param: any) => {
|
||||||
|
const paramId = Number(param.id)
|
||||||
|
if (currentPageSelectedIds.has(paramId)) {
|
||||||
|
selectedParamIds.value.add(paramId)
|
||||||
|
} else {
|
||||||
|
selectedParamIds.value.delete(paramId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
syncParamSelectionCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 参数查询
|
// 参数查询
|
||||||
@@ -1001,16 +1062,9 @@ const handleSaveParamConfig = async () => {
|
|||||||
try {
|
try {
|
||||||
paramConfigSubmitting.value = true
|
paramConfigSubmitting.value = true
|
||||||
|
|
||||||
const selectedParams = paramConfigTableRef.value?.getSelectionRows() || []
|
const selectedIds = new Set<number>(Array.from(selectedParamIds.value).map(id => Number(id)))
|
||||||
const selectedParamIds = new Set<number>(
|
const toBind = Array.from(selectedIds).filter((id: number) => !boundParamIds.value.has(id))
|
||||||
selectedParams
|
const toUnbind = Array.from(boundParamIds.value).filter((id: number) => !selectedIds.has(id))
|
||||||
.map((p: any) => p.id)
|
|
||||||
.filter((id: any) => id !== null && id !== undefined)
|
|
||||||
.map((id: any) => Number(id)),
|
|
||||||
)
|
|
||||||
|
|
||||||
const toBind = Array.from(selectedParamIds).filter((id: number) => !boundParamIds.value.has(id))
|
|
||||||
const toUnbind = Array.from(boundParamIds.value).filter((id: number) => !selectedParamIds.has(id))
|
|
||||||
|
|
||||||
const currentDevId = Number(currentDevice.value.id)
|
const currentDevId = Number(currentDevice.value.id)
|
||||||
|
|
||||||
@@ -1022,7 +1076,11 @@ const handleSaveParamConfig = async () => {
|
|||||||
ElMessage.success('参数配置保存成功')
|
ElMessage.success('参数配置保存成功')
|
||||||
paramConfigDialogVisible.value = false
|
paramConfigDialogVisible.value = false
|
||||||
|
|
||||||
boundParamIds.value = selectedParamIds
|
boundParamIds.value = new Set(selectedIds)
|
||||||
|
syncParamSelectionCache()
|
||||||
|
if (currentDevice.value?.id) {
|
||||||
|
delete paramSelectionCache.value[String(currentDevice.value.id)]
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存参数配置失败:', error)
|
console.error('保存参数配置失败:', error)
|
||||||
ElMessage.error('保存参数配置失败')
|
ElMessage.error('保存参数配置失败')
|
||||||
@@ -1037,6 +1095,11 @@ const handleParamConfigDialogClose = () => {
|
|||||||
paramList.value = []
|
paramList.value = []
|
||||||
paramTotal.value = 0
|
paramTotal.value = 0
|
||||||
boundParamIds.value = new Set()
|
boundParamIds.value = new Set()
|
||||||
|
selectedParamIds.value = new Set()
|
||||||
|
if (currentDevice.value?.id) {
|
||||||
|
delete paramSelectionCache.value[String(currentDevice.value.id)]
|
||||||
|
}
|
||||||
|
syncingParamSelection.value = false
|
||||||
paramQueryForm.paramTypeData = ''
|
paramQueryForm.paramTypeData = ''
|
||||||
paramPagination.pageNum = 1
|
paramPagination.pageNum = 1
|
||||||
paramPagination.pageSize = 10
|
paramPagination.pageSize = 10
|
||||||
|
|||||||
637
rc_autoplc_front/src/views/statusmonitor/index.vue
Normal file
637
rc_autoplc_front/src/views/statusmonitor/index.vue
Normal file
@@ -0,0 +1,637 @@
|
|||||||
|
<template>
|
||||||
|
<div class="status-monitor-page">
|
||||||
|
<el-breadcrumb separator="/" class="page-breadcrumb">
|
||||||
|
<el-breadcrumb-item><router-link to="/home">首页</router-link></el-breadcrumb-item>
|
||||||
|
<el-breadcrumb-item>业务流程控制</el-breadcrumb-item>
|
||||||
|
<el-breadcrumb-item>状态监控界面</el-breadcrumb-item>
|
||||||
|
</el-breadcrumb>
|
||||||
|
|
||||||
|
<div class="monitor-layout">
|
||||||
|
<el-card class="left-panel" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>功能岛列表</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-loading="islandLoading" class="island-scroll">
|
||||||
|
<div v-if="islands.length === 0" class="empty-block">
|
||||||
|
<el-empty description="暂无功能岛数据" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="island-list">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in islands"
|
||||||
|
:key="item.id ?? index"
|
||||||
|
class="monitor-card island-card"
|
||||||
|
:class="{ active: selectedIslandId === getItemId(item) }"
|
||||||
|
@click="selectIsland(item)"
|
||||||
|
>
|
||||||
|
<div class="island-card-header">
|
||||||
|
<div class="monitor-card-index">{{ index + 1 }}</div>
|
||||||
|
<div class="monitor-card-main island-card-main">
|
||||||
|
<div class="monitor-card-title">{{ getIslandName(item) }}</div>
|
||||||
|
<div class="monitor-card-subtitle">{{ getIslandDesc(item) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-tag class="island-status-tag" :type="getStatusTag(getIslandStatus(item))" size="small" effect="light">
|
||||||
|
{{ getStatusText(getIslandStatus(item)) }}
|
||||||
|
</el-tag>
|
||||||
|
|
||||||
|
<div class="action-badges" v-if="getIslandActionList(item).length > 0">
|
||||||
|
<div
|
||||||
|
v-for="action in getIslandActionList(item).slice(0, 2)"
|
||||||
|
:key="getItemId(action)"
|
||||||
|
class="action-badge"
|
||||||
|
>
|
||||||
|
<span class="action-badge-label">绑定动作</span>
|
||||||
|
<span class="action-badge-name">{{ getActionName(action) }}</span>
|
||||||
|
<el-tag :type="getStatusTag(getActionStatus(action))" size="small" effect="light" round>
|
||||||
|
{{ getStatusText(getActionStatus(action)) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<span v-if="getIslandActionList(item).length > 2" class="action-badge-more">
|
||||||
|
+{{ getIslandActionList(item).length - 2 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="right-panel" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>状态层级监控</span>
|
||||||
|
<span class="panel-hint">功能岛 → 动作 → 参数</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-loading="detailLoading" class="detail-wrap">
|
||||||
|
<template v-if="selectedIsland">
|
||||||
|
<div class="level-block">
|
||||||
|
<div class="level-title">功能岛</div>
|
||||||
|
<div
|
||||||
|
class="monitor-card island-detail-card"
|
||||||
|
:class="{ active: activeDialogType === 'island' }"
|
||||||
|
@click="openIslandDialog(selectedIsland)"
|
||||||
|
>
|
||||||
|
<div class="monitor-card-index">{{ getIslandDisplayIndex(selectedIsland) }}</div>
|
||||||
|
<div class="monitor-card-main">
|
||||||
|
<div class="monitor-card-title">{{ getIslandName(selectedIsland) }}</div>
|
||||||
|
<div class="monitor-card-subtitle">{{ getIslandDesc(selectedIsland) }}</div>
|
||||||
|
</div>
|
||||||
|
<el-tag :type="getStatusTag(getIslandStatus(selectedIsland))" effect="light" size="small">
|
||||||
|
{{ getStatusText(getIslandStatus(selectedIsland)) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="actionsToRender.length > 0" class="level-block actions-block">
|
||||||
|
<div class="actions-row" :style="{ '--actions-count': actionsToRender.length }">
|
||||||
|
<div v-for="(group, actionIndex) in actionsToRender" :key="getItemId(group.action) ?? actionIndex" class="action-column">
|
||||||
|
<div class="arrow-down" aria-hidden="true">
|
||||||
|
<el-icon><Bottom /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="monitor-card action-card"
|
||||||
|
:class="{ active: activeDialogType === 'action' && selectedActionId === getItemId(group.action) }"
|
||||||
|
@click="openActionDialog(group.action)"
|
||||||
|
>
|
||||||
|
<div class="monitor-card-index">{{ actionIndex + 1 }}</div>
|
||||||
|
<div class="monitor-card-main">
|
||||||
|
<div class="monitor-card-title">{{ getActionName(group.action) }}</div>
|
||||||
|
<div class="monitor-card-subtitle">{{ getActionDesc(group.action) }}</div>
|
||||||
|
</div>
|
||||||
|
<el-tag :type="getStatusTag(getActionStatus(group.action))" effect="light" size="small">
|
||||||
|
{{ getStatusText(getActionStatus(group.action)) }}</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="arrow-down" aria-hidden="true">
|
||||||
|
<el-icon><Bottom /></el-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-list">
|
||||||
|
<div v-for="(param, paramIndex) in group.params" :key="getItemId(param) ?? paramIndex" class="param-card monitor-card">
|
||||||
|
<div class="monitor-card-index">{{ getParamIconText(param) }}</div>
|
||||||
|
<div class="monitor-card-main">
|
||||||
|
<div class="monitor-card-title">{{ getParamName(param) }}</div>
|
||||||
|
<div class="monitor-card-subtitle">
|
||||||
|
默认值:{{ getParamDefaultValue(param) }} | 单位:{{ getParamUnit(param) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="group.params.length === 0" class="param-empty">暂无参数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-else description="当前功能岛暂无动作数据" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-empty v-else description="请选择功能岛查看状态" />
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-dialog v-model="islandDialogVisible" title="功能岛状态详情" width="640px" destroy-on-close>
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="功能岛名称">{{ getIslandName(dialogIsland) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="功能岛编码">{{ getIslandCode(dialogIsland) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="功能岛状态">
|
||||||
|
<el-tag :type="getStatusTag(getIslandStatus(dialogIsland))" effect="light">
|
||||||
|
{{ getStatusText(getIslandStatus(dialogIsland)) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="功能岛描述">{{ getIslandDesc(dialogIsland) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="actionDialogVisible" title="动作状态详情" width="640px" destroy-on-close>
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="动作名称">{{ getActionName(dialogAction) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="所属功能岛">{{ getIslandName(selectedIsland) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="动作状态">
|
||||||
|
<el-tag :type="getStatusTag(getActionStatus(dialogAction))" effect="light">
|
||||||
|
{{ getStatusText(getActionStatus(dialogAction)) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="动作描述">{{ getActionDesc(dialogAction) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Bottom } from '@element-plus/icons-vue'
|
||||||
|
import { islandInfolist } from '@/api/tb/islandinfo'
|
||||||
|
import { devInfolist } from '@/api/tb/devinfo'
|
||||||
|
import { devparamselect } from '@/api/tb/devparam'
|
||||||
|
|
||||||
|
interface AnyItem {
|
||||||
|
id?: string | number
|
||||||
|
islandId?: string | number
|
||||||
|
devId?: string | number
|
||||||
|
paramId?: string | number
|
||||||
|
islandName?: string
|
||||||
|
name?: string
|
||||||
|
islandDesc?: string
|
||||||
|
desc?: string
|
||||||
|
devName?: string
|
||||||
|
actionName?: string
|
||||||
|
devDesc?: string
|
||||||
|
actionDesc?: string
|
||||||
|
paramName?: string
|
||||||
|
parName?: string
|
||||||
|
paramValue?: string | number
|
||||||
|
parValue?: string | number
|
||||||
|
paramDesc?: string
|
||||||
|
parDesc?: string
|
||||||
|
parmList?: string | number
|
||||||
|
status?: number | string
|
||||||
|
state?: number | string
|
||||||
|
onlineStatus?: number | string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const islandLoading = ref(false)
|
||||||
|
const detailLoading = ref(false)
|
||||||
|
const islands = ref<AnyItem[]>([])
|
||||||
|
const actions = ref<AnyItem[]>([])
|
||||||
|
const params = ref<AnyItem[]>([])
|
||||||
|
|
||||||
|
const selectedIslandId = ref<string | number | ''>('')
|
||||||
|
const selectedActionId = ref<string | number | ''>('')
|
||||||
|
const activeDialogType = ref<'island' | 'action' | ''>('')
|
||||||
|
const islandDialogVisible = ref(false)
|
||||||
|
const actionDialogVisible = ref(false)
|
||||||
|
const dialogIsland = ref<AnyItem>({})
|
||||||
|
const dialogAction = ref<AnyItem>({})
|
||||||
|
|
||||||
|
const selectedIsland = computed(() => islands.value.find(item => String(getItemId(item)) === String(selectedIslandId.value)) || null)
|
||||||
|
|
||||||
|
const getResponseList = (res: any) => {
|
||||||
|
const data = res?.data?.data ?? res?.data ?? res ?? {}
|
||||||
|
if (Array.isArray(data)) return data
|
||||||
|
return data.records || data.list || data.rows || data.items || data.data || []
|
||||||
|
}
|
||||||
|
|
||||||
|
const getItemId = (item: AnyItem) => item?.id ?? item?.islandId ?? item?.devId ?? item?.paramId ?? ''
|
||||||
|
const getIslandName = (item?: AnyItem | null) => item?.islandName || item?.name || '未命名功能岛'
|
||||||
|
const getIslandDesc = (item?: AnyItem | null) => item?.desc || item?.islandDesc || '暂无描述'
|
||||||
|
const getActionName = (item?: AnyItem | null) => item?.devName || item?.actionName || item?.name || '未命名动作'
|
||||||
|
const getActionDesc = (item?: AnyItem | null) => item?.devDesc || item?.actionDesc || item?.desc || '暂无描述'
|
||||||
|
const getParamName = (item?: AnyItem | null) => item?.paramName || item?.parName || item?.name || '未命名参数'
|
||||||
|
const getParamDefaultValue = (item?: AnyItem | null) => {
|
||||||
|
const value = item?.paramValue ?? item?.parValue ?? item?.value ?? item?.defaultValue ?? item?.defaultVal ?? item?.initValue
|
||||||
|
return value !== undefined && value !== null && value !== '' ? String(value) : '—'
|
||||||
|
}
|
||||||
|
const getParamUnit = (item?: AnyItem | null) => item?.paramUnit || item?.parUnit || item?.unit || '—'
|
||||||
|
const getParamIconText = (item?: AnyItem | null) => {
|
||||||
|
const name = getParamName(item)
|
||||||
|
return name ? name.slice(0, 1) : '参'
|
||||||
|
}
|
||||||
|
const getIslandDisplayIndex = (item?: AnyItem | null) => {
|
||||||
|
const index = islands.value.findIndex(island => String(getItemId(island)) === String(getItemId(item ?? {})))
|
||||||
|
return index >= 0 ? String(index + 1).padStart(3, '0') : '000'
|
||||||
|
}
|
||||||
|
const getIslandCode = (item?: AnyItem | null) => item?.islandCode || item?.code || item?.id || '-'
|
||||||
|
|
||||||
|
const normalizeStatus = (value: any) => {
|
||||||
|
if (value === 0 || value === '0' || value === 'offline' || value === 'OFFLINE' || value === '离线') return 0
|
||||||
|
if (value === 1 || value === '1' || value === 'idle' || value === 'IDLE' || value === '空闲') return 1
|
||||||
|
if (value === 2 || value === '2' || value === 'busy' || value === 'BUSY' || value === '忙碌') return 2
|
||||||
|
if (value === 5 || value === '5' || value === 'fault' || value === 'FAULT' || value === '故障') return 5
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const getIslandStatus = (item?: AnyItem | null) => normalizeStatus(item?.status ?? item?.state ?? item?.onlineStatus)
|
||||||
|
const getActionStatus = (item?: AnyItem | null) => normalizeStatus(item?.status ?? item?.state ?? item?.onlineStatus)
|
||||||
|
|
||||||
|
const getStatusText = (status?: number) => {
|
||||||
|
if (status === 0) return '离线'
|
||||||
|
if (status === 1) return '空闲'
|
||||||
|
if (status === 2) return '忙碌'
|
||||||
|
if (status === 5) return '故障'
|
||||||
|
return '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusTag = (status?: number) => {
|
||||||
|
if (status === 0) return 'info'
|
||||||
|
if (status === 1) return 'success'
|
||||||
|
if (status === 2) return 'warning'
|
||||||
|
if (status === 5) return 'danger'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
islandLoading.value = true
|
||||||
|
detailLoading.value = true
|
||||||
|
try {
|
||||||
|
const [islandRes, actionRes, paramRes] = await Promise.all([
|
||||||
|
islandInfolist({ pageNum: 1, pageSize: 1000 }),
|
||||||
|
devInfolist({ pageNum: 1, pageSize: 1000, devModelNot: 'PLC' }),
|
||||||
|
devparamselect({ pageNum: 1, pageSize: 5000 }),
|
||||||
|
])
|
||||||
|
|
||||||
|
islands.value = getResponseList(islandRes)
|
||||||
|
actions.value = getResponseList(actionRes)
|
||||||
|
params.value = getResponseList(paramRes)
|
||||||
|
|
||||||
|
const firstIsland = islands.value[0]
|
||||||
|
if (firstIsland) {
|
||||||
|
selectedIslandId.value = getItemId(firstIsland)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error)
|
||||||
|
ElMessage.error(error?.message || '获取状态监控数据失败')
|
||||||
|
} finally {
|
||||||
|
islandLoading.value = false
|
||||||
|
detailLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectIsland = (item: AnyItem) => {
|
||||||
|
selectedIslandId.value = getItemId(item)
|
||||||
|
selectedActionId.value = ''
|
||||||
|
activeDialogType.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIslandRelatedActions = (islandId: string | number | '') => {
|
||||||
|
if (islandId === '') return []
|
||||||
|
return actions.value.filter(item => String(item.islandId ?? item.parentIslandId ?? item.functionIslandId ?? '') === String(islandId))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIslandActionList = (item?: AnyItem | null) => getIslandRelatedActions(getItemId(item ?? {}))
|
||||||
|
|
||||||
|
const getParamsForAction = (action: AnyItem) => {
|
||||||
|
const actionId = getItemId(action)
|
||||||
|
if (actionId === '') return []
|
||||||
|
|
||||||
|
return params.value.filter(item => {
|
||||||
|
const rawDevIds = item.devIds
|
||||||
|
const devIds = Array.isArray(rawDevIds)
|
||||||
|
? rawDevIds
|
||||||
|
: String(rawDevIds ?? '')
|
||||||
|
.split(/[;,,]/)
|
||||||
|
.map(id => id.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
return devIds.some(id => String(id) === String(actionId))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openIslandDialog = (item: AnyItem) => {
|
||||||
|
dialogIsland.value = item
|
||||||
|
activeDialogType.value = 'island'
|
||||||
|
islandDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openActionDialog = (item: AnyItem) => {
|
||||||
|
dialogAction.value = item
|
||||||
|
selectedActionId.value = getItemId(item)
|
||||||
|
activeDialogType.value = 'action'
|
||||||
|
actionDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionsToRender = computed(() => getIslandRelatedActions(selectedIslandId.value).map(action => ({
|
||||||
|
action,
|
||||||
|
params: getParamsForAction(action),
|
||||||
|
})))
|
||||||
|
|
||||||
|
onMounted(loadData)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-monitor-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-breadcrumb {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 360px minmax(0, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel,
|
||||||
|
.right-panel {
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-hint {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.island-scroll,
|
||||||
|
.detail-wrap {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.island-list,
|
||||||
|
.param-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-card {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
background: #fff;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.island-card {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.island-card-header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.island-status-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.island-card-main {
|
||||||
|
padding-right: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-card:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-card.active {
|
||||||
|
border-color: #67c23a;
|
||||||
|
box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-card-index {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
flex: 0 0 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #79aefc 0%, #2f7de1 100%);
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-card-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.island-card-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-card-subtitle {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
display: -webkit-box;
|
||||||
|
line-clamp: 1;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e5eaf3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-badge-label {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-badge-name {
|
||||||
|
max-width: 150px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-badge-more {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
background: rgba(64, 158, 255, 0.08);
|
||||||
|
border: 1px dashed rgba(64, 158, 255, 0.35);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-title,
|
||||||
|
.param-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--actions-count), minmax(0, 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
align-items: start;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-column {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 340px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-node-wrap {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
width: 100%;
|
||||||
|
height: 86px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: linear-gradient(180deg, rgba(64, 158, 255, 0.03), rgba(103, 194, 58, 0.03));
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-arrow,
|
||||||
|
.arrow-down {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 24px;
|
||||||
|
color: #67c23a;
|
||||||
|
font-size: 22px;
|
||||||
|
margin: 2px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 340px;
|
||||||
|
min-height: 72px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
background: #fafcff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-value {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-empty,
|
||||||
|
.empty-block {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 120px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1201px) {
|
||||||
|
.actions-row {
|
||||||
|
grid-template-columns: repeat(var(--actions-count), minmax(340px, 1fr));
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.monitor-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user