<template>
|
<div class="device-group-list">
|
<!-- 搜索和筛选区域 -->
|
<div class="search-section">
|
<el-form :model="searchForm" :inline="true" class="search-form">
|
<el-form-item label="组类型">
|
<el-select v-model="searchForm.groupType" placeholder="选择组类型" clearable>
|
<el-option label="设备组" value="设备组" />
|
<el-option label="管理组" value="管理组" />
|
<el-option label="监控组" value="监控组" />
|
<el-option label="维护组" value="维护组" />
|
</el-select>
|
</el-form-item>
|
<el-form-item label="组状态">
|
<el-select v-model="searchForm.groupStatus" placeholder="选择组状态" clearable>
|
<el-option label="启用" value="ENABLED" />
|
<el-option label="禁用" value="DISABLED" />
|
</el-select>
|
</el-form-item>
|
<el-form-item label="搜索关键词">
|
<el-input v-model="searchForm.keyword" placeholder="组名称或描述" clearable style="width: 200px;">
|
<template #append>
|
<el-button @click="handleSearch">
|
<el-icon><Search /></el-icon>
|
</el-button>
|
</template>
|
</el-input>
|
</el-form-item>
|
<el-form-item>
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
<el-button @click="resetSearch">重置</el-button>
|
</el-form-item>
|
</el-form>
|
</div>
|
|
<!-- 批量操作区域 -->
|
<div class="batch-operation" v-if="selectedGroups.length > 0">
|
<el-alert
|
:title="`已选择 ${selectedGroups.length} 个设备组`"
|
type="info"
|
show-icon
|
:closable="false"
|
/>
|
<div class="batch-buttons">
|
<el-button type="success" size="small" @click="batchEnable">批量启用</el-button>
|
<el-button type="warning" size="small" @click="batchDisable">批量禁用</el-button>
|
<el-button type="danger" size="small" @click="batchDelete">批量删除</el-button>
|
<el-button size="small" @click="clearSelection">取消选择</el-button>
|
</div>
|
</div>
|
|
<!-- 设备组列表 -->
|
<div class="table-section">
|
<el-table
|
ref="groupTable"
|
v-loading="tableLoading"
|
:data="groupList"
|
@selection-change="handleSelectionChange"
|
border
|
stripe
|
style="width: 100%"
|
>
|
<el-table-column type="selection" width="55" />
|
<el-table-column prop="groupName" label="组名称" min-width="150" />
|
<el-table-column prop="groupCode" label="组编码" width="130" />
|
<el-table-column prop="groupType" label="组类型" width="100">
|
<template #default="scope">
|
<el-tag :type="getGroupTypeTag(scope.row.groupType)">
|
{{ scope.row.groupType }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="description" label="描述" min-width="200" />
|
<el-table-column prop="groupStatus" label="组状态" width="100">
|
<template #default="scope">
|
<el-tag :type="getGroupStatusTag(scope.row.groupStatus)" size="small">
|
{{ getGroupStatusText(scope.row.groupStatus) }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="enabled" label="启用状态" width="100">
|
<template #default="scope">
|
<el-switch
|
v-model="scope.row.enabled"
|
@change="handleStatusChange(scope.row)"
|
/>
|
</template>
|
</el-table-column>
|
<el-table-column prop="deviceCount" label="设备数量" width="100" align="center">
|
<template #default="scope">
|
<el-tag type="info" size="small">{{ scope.row.deviceCount || 0 }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="onlineDeviceCount" label="在线设备" width="100" align="center">
|
<template #default="scope">
|
<el-tag type="success" size="small">{{ scope.row.onlineDeviceCount || 0 }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="sortOrder" label="排序" width="80" align="center" />
|
<el-table-column label="操作" width="280" fixed="right">
|
<template #default="scope">
|
<el-button type="primary" size="small" @click="editGroup(scope.row)">
|
编辑
|
</el-button>
|
<el-button type="success" size="small" @click="manageDevices(scope.row)">
|
设备管理
|
</el-button>
|
<el-button type="warning" size="small" :loading="groupPlcLoading" @click="triggerGroupPlcRequest(scope.row)">
|
PLC请求
|
</el-button>
|
<el-button type="info" size="small" @click="viewGroupPlcStatus(scope.row)">
|
PLC状态
|
</el-button>
|
<el-button type="success" size="small" @click="viewStatistics(scope.row)">
|
统计
|
</el-button>
|
<el-dropdown @command="(command) => handleCommand(command, scope.row)">
|
<el-button type="info" size="small">
|
更多<el-icon><ArrowDown /></el-icon>
|
</el-button>
|
<template #dropdown>
|
<el-dropdown-menu>
|
<el-dropdown-item command="view">查看详情</el-dropdown-item>
|
<el-dropdown-item command="copy">复制配置</el-dropdown-item>
|
<el-dropdown-item command="export">导出配置</el-dropdown-item>
|
<el-dropdown-item command="test">测试连接</el-dropdown-item>
|
<el-dropdown-item command="plc-report">PLC汇报</el-dropdown-item>
|
<el-dropdown-item command="delete" divided>删除组</el-dropdown-item>
|
</el-dropdown-menu>
|
</template>
|
</el-dropdown>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
|
<!-- 分页 -->
|
<div class="pagination-section">
|
<el-pagination
|
v-model:current-page="pagination.page"
|
v-model:page-size="pagination.size"
|
:page-sizes="[10, 20, 50, 100]"
|
:total="pagination.total"
|
layout="total, sizes, prev, pager, next, jumper"
|
@size-change="handleSizeChange"
|
@current-change="handleCurrentChange"
|
/>
|
</div>
|
|
<!-- 设备管理弹窗 -->
|
<el-dialog
|
v-model="deviceDialogVisible"
|
:title="`设备组:${currentGroup?.groupName} - 设备管理`"
|
width="80%"
|
:close-on-click-modal="false"
|
>
|
<div class="device-management">
|
<div class="dialog-header">
|
<div class="device-stats">
|
<el-statistic title="总设备数" :value="currentGroup?.deviceCount || 0" />
|
<el-statistic title="在线设备" :value="currentGroup?.onlineDeviceCount || 0" />
|
<el-statistic title="离线设备" :value="(currentGroup?.deviceCount || 0) - (currentGroup?.onlineDeviceCount || 0)" />
|
</div>
|
<div class="dialog-buttons">
|
<el-button type="primary" @click="addDevices">添加设备</el-button>
|
<el-button type="danger" @click="removeDevices" :disabled="selectedDevicesInGroup.length === 0">
|
移除设备
|
</el-button>
|
</div>
|
</div>
|
|
<el-table
|
v-loading="deviceLoading"
|
:data="groupDeviceList"
|
@selection-change="handleDeviceSelectionChange"
|
border
|
stripe
|
>
|
<el-table-column type="selection" width="55" />
|
<el-table-column prop="deviceName" label="设备名称" />
|
<el-table-column prop="deviceCode" label="设备编码" />
|
<el-table-column prop="deviceType" label="设备类型" />
|
<el-table-column prop="plcIp" label="PLC IP" />
|
<el-table-column prop="deviceStatus" label="设备状态">
|
<template #default="scope">
|
<el-tag :type="getDeviceStatusTag(scope.row.deviceStatus)" size="small">
|
{{ getDeviceStatusText(scope.row.deviceStatus) }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
|
<template #footer>
|
<el-button @click="deviceDialogVisible = false">关闭</el-button>
|
</template>
|
</el-dialog>
|
|
<!-- 统计详情弹窗 -->
|
<el-dialog
|
v-model="statisticsDialogVisible"
|
title="设备组统计"
|
width="60%"
|
>
|
<div class="statistics-content">
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<h4>设备类型分布</h4>
|
<div class="chart-container" style="height: 200px; background: #f5f7fa; display: flex; align-items: center; justify-content: center;">
|
<p>图表功能开发中...</p>
|
</div>
|
</el-col>
|
<el-col :span="12">
|
<h4>设备状态分布</h4>
|
<div class="chart-container" style="height: 200px; background: #f5f7fa; display: flex; align-items: center; justify-content: center;">
|
<p>图表功能开发中...</p>
|
</div>
|
</el-col>
|
</el-row>
|
<el-row :gutter="20" style="margin-top: 20px;">
|
<el-col :span="24">
|
<h4>详细信息</h4>
|
<el-descriptions :column="2" border>
|
<el-descriptions-item label="总设备数">{{ currentGroup?.deviceCount || 0 }}</el-descriptions-item>
|
<el-descriptions-item label="在线设备">{{ currentGroup?.onlineDeviceCount || 0 }}</el-descriptions-item>
|
<el-descriptions-item label="离线设备">{{ (currentGroup?.deviceCount || 0) - (currentGroup?.onlineDeviceCount || 0) }}</el-descriptions-item>
|
<el-descriptions-item label="启用率">{{ getEnableRate() }}%</el-descriptions-item>
|
</el-descriptions>
|
</el-col>
|
</el-row>
|
</div>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted } from 'vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { Search, ArrowDown } from '@element-plus/icons-vue'
|
import { deviceGroupApi, devicePlcApi } from '@/api/device/deviceManagement'
|
|
// 响应式数据
|
const groupTable = ref(null)
|
const tableLoading = ref(false)
|
const groupList = ref([])
|
const selectedGroups = ref([])
|
const groupPlcLoading = ref(false)
|
|
// 搜索表单
|
const searchForm = reactive({
|
groupType: '',
|
groupStatus: '',
|
keyword: ''
|
})
|
|
// 分页信息
|
const pagination = reactive({
|
page: 1,
|
size: 10,
|
total: 0
|
})
|
|
// 设备管理弹窗相关
|
const deviceDialogVisible = ref(false)
|
const deviceLoading = ref(false)
|
const currentGroup = ref(null)
|
const groupDeviceList = ref([])
|
const selectedDevicesInGroup = ref([])
|
|
// 统计弹窗
|
const statisticsDialogVisible = ref(false)
|
|
// 事件定义
|
const emit = defineEmits(['group-selected', 'refresh-statistics'])
|
|
// 方法定义
|
const loadGroupList = async () => {
|
try {
|
tableLoading.value = true
|
const params = {
|
page: pagination.page,
|
size: pagination.size,
|
groupType: searchForm.groupType || undefined,
|
groupStatus: searchForm.groupStatus || undefined,
|
keyword: searchForm.keyword || undefined
|
}
|
|
const response = await deviceGroupApi.getList(params)
|
// MyBatis-Plus Page 对象结构:{ records: [], total: 0 }
|
if (response && response.data) {
|
groupList.value = response.data.records || response.data.content || response.data.list || []
|
pagination.total = response.data.total || response.data.totalElements || 0
|
} else {
|
groupList.value = []
|
pagination.total = 0
|
}
|
} catch (error) {
|
console.error('加载设备组列表失败:', error)
|
ElMessage.error('加载设备组列表失败: ' + (error.response?.data?.message || error.message))
|
groupList.value = []
|
pagination.total = 0
|
} finally {
|
tableLoading.value = false
|
}
|
}
|
|
const handleSearch = () => {
|
pagination.page = 1
|
loadGroupList()
|
}
|
|
const resetSearch = () => {
|
searchForm.groupType = ''
|
searchForm.groupStatus = ''
|
searchForm.keyword = ''
|
pagination.page = 1
|
loadGroupList()
|
}
|
|
const handleSelectionChange = (selection) => {
|
selectedGroups.value = selection
|
}
|
|
const clearSelection = () => {
|
groupTable.value?.clearSelection()
|
selectedGroups.value = []
|
}
|
|
const groupPlcLabelMap = {
|
request: 'PLC请求',
|
report: 'PLC汇报'
|
}
|
|
const handleGroupPlcOperation = async (groupId, operation) => {
|
if (!groupId) {
|
ElMessage.warning('请先选择设备组')
|
return
|
}
|
groupPlcLoading.value = true
|
try {
|
let response
|
switch (operation) {
|
case 'request':
|
response = await devicePlcApi.triggerGroupRequest(groupId)
|
break
|
case 'report':
|
response = await devicePlcApi.triggerGroupReport(groupId)
|
break
|
default:
|
throw new Error('未知的设备组PLC操作')
|
}
|
const results = response?.data || []
|
const successCount = results.filter(item => item.success).length
|
const label = groupPlcLabelMap[operation] || 'PLC操作'
|
if (results.length === 0) {
|
ElMessage.info(`${label}未返回结果`)
|
} else if (successCount === results.length) {
|
ElMessage.success(`${label}成功(${successCount}/${results.length})`)
|
} else {
|
ElMessage.warning(`${label}部分成功(${successCount}/${results.length})`)
|
}
|
} catch (error) {
|
const label = groupPlcLabelMap[operation] || 'PLC操作'
|
console.error('设备组PLC操作失败:', error)
|
ElMessage.error(`${label}失败:${error.response?.data?.message || error.message}`)
|
} finally {
|
groupPlcLoading.value = false
|
}
|
}
|
|
const triggerGroupPlcRequest = (row) => handleGroupPlcOperation(row.id || row.groupId, 'request')
|
const triggerGroupPlcReport = (row) => handleGroupPlcOperation(row.id || row.groupId, 'report')
|
|
const viewGroupPlcStatus = async (row) => {
|
try {
|
const response = await devicePlcApi.getGroupStatus(row.id || row.groupId)
|
const statusList = response?.data || []
|
if (!statusList.length) {
|
ElMessage.info('未获取到PLC状态数据')
|
return
|
}
|
const htmlContent = statusList.map(item => {
|
const header = `${item.deviceName || item.deviceCode || item.deviceId}`
|
const body = JSON.stringify(item.data || {}, null, 2)
|
return `<div style="margin-bottom:12px"><strong>${header}</strong><pre style="margin:4px 0 0;max-height:260px;overflow:auto;">${body}</pre></div>`
|
}).join('')
|
await ElMessageBox.alert(htmlContent, `PLC状态 - ${row.groupName || row.groupCode}`, {
|
dangerouslyUseHTMLString: true,
|
confirmButtonText: '关闭'
|
})
|
} catch (error) {
|
console.error('获取设备组PLC状态失败:', error)
|
ElMessage.error('获取设备组PLC状态失败')
|
}
|
}
|
|
const handleStatusChange = async (row) => {
|
try {
|
const groupId = row.id || row.groupId
|
if (row.enabled) {
|
await deviceGroupApi.enable(groupId)
|
ElMessage.success('设备组启用成功')
|
} else {
|
await deviceGroupApi.disable(groupId)
|
ElMessage.success('设备组禁用成功')
|
}
|
emit('refresh-statistics')
|
loadGroupList() // 刷新列表
|
} catch (error) {
|
console.error('更新设备组状态失败:', error)
|
row.enabled = !row.enabled // 恢复状态
|
ElMessage.error('更新设备组状态失败: ' + (error.response?.data?.message || error.message))
|
}
|
}
|
|
const batchEnable = async () => {
|
try {
|
const groupIds = selectedGroups.value.map(item => item.id || item.groupId)
|
await deviceGroupApi.batchEnable(groupIds)
|
ElMessage.success(`成功启用 ${groupIds.length} 个设备组`)
|
clearSelection()
|
loadGroupList()
|
emit('refresh-statistics')
|
} catch (error) {
|
console.error('批量启用失败:', error)
|
ElMessage.error('批量启用失败: ' + (error.response?.data?.message || error.message))
|
}
|
}
|
|
const batchDisable = async () => {
|
try {
|
const groupIds = selectedGroups.value.map(item => item.id || item.groupId)
|
await deviceGroupApi.batchDisable(groupIds)
|
ElMessage.success(`成功禁用 ${groupIds.length} 个设备组`)
|
clearSelection()
|
loadGroupList()
|
emit('refresh-statistics')
|
} catch (error) {
|
console.error('批量禁用失败:', error)
|
ElMessage.error('批量禁用失败: ' + (error.response?.data?.message || error.message))
|
}
|
}
|
|
const batchDelete = async () => {
|
try {
|
await ElMessageBox.confirm(
|
`确定要删除选中的 ${selectedGroups.value.length} 个设备组吗?此操作不可恢复!`,
|
'批量删除确认',
|
{
|
confirmButtonText: '确定删除',
|
cancelButtonText: '取消',
|
type: 'warning'
|
}
|
)
|
|
const groupIds = selectedGroups.value.map(item => item.id || item.groupId)
|
// 逐个删除
|
for (const groupId of groupIds) {
|
await deviceGroupApi.delete({ groupId })
|
}
|
ElMessage.success(`成功删除 ${groupIds.length} 个设备组`)
|
clearSelection()
|
loadGroupList()
|
emit('refresh-statistics')
|
} catch (error) {
|
if (error !== 'cancel') {
|
console.error('批量删除失败:', error)
|
ElMessage.error('批量删除失败')
|
}
|
}
|
}
|
|
const editGroup = (row) => {
|
emit('group-selected', row)
|
}
|
|
const manageDevices = async (row) => {
|
currentGroup.value = row
|
deviceDialogVisible.value = true
|
await loadGroupDevices(row.id || row.groupId)
|
}
|
|
const loadGroupDevices = async (groupId) => {
|
try {
|
deviceLoading.value = true
|
const response = await deviceGroupApi.getGroupDevices(groupId)
|
if (response && response.data) {
|
groupDeviceList.value = response.data || []
|
} else {
|
groupDeviceList.value = []
|
}
|
} catch (error) {
|
console.error('加载组设备列表失败:', error)
|
ElMessage.error('加载组设备列表失败: ' + (error.response?.data?.message || error.message))
|
groupDeviceList.value = []
|
} finally {
|
deviceLoading.value = false
|
}
|
}
|
|
const viewStatistics = (row) => {
|
currentGroup.value = row
|
statisticsDialogVisible.value = true
|
}
|
|
const handleDeviceSelectionChange = (selection) => {
|
selectedDevicesInGroup.value = selection
|
}
|
|
const addDevices = () => {
|
// 添加设备到组逻辑
|
ElMessage.info('添加设备功能开发中...')
|
}
|
|
const removeDevices = async () => {
|
try {
|
if (selectedDevicesInGroup.value.length === 0) {
|
ElMessage.warning('请选择要移除的设备')
|
return
|
}
|
|
await ElMessageBox.confirm(
|
`确定要从设备组中移除选中的 ${selectedDevicesInGroup.value.length} 个设备吗?`,
|
'移除设备确认'
|
)
|
|
const deviceIds = selectedDevicesInGroup.value.map(item => item.id || item.deviceId)
|
// 批量移除设备
|
await deviceGroupApi.batchRemoveDevicesFromGroup({
|
groupId: currentGroup.value.id || currentGroup.value.groupId,
|
deviceIds
|
})
|
ElMessage.success(`成功移除 ${deviceIds.length} 个设备`)
|
loadGroupDevices(currentGroup.value.id || currentGroup.value.groupId)
|
} catch (error) {
|
if (error !== 'cancel') {
|
console.error('移除设备失败:', error)
|
ElMessage.error('移除设备失败')
|
}
|
}
|
}
|
|
const handleCommand = async (command, row) => {
|
switch (command) {
|
case 'view':
|
// 查看详情逻辑
|
ElMessage.info('查看详情功能开发中...')
|
break
|
case 'copy':
|
// 复制配置逻辑
|
ElMessage.info('复制配置功能开发中...')
|
break
|
case 'export':
|
// 导出配置逻辑
|
ElMessage.info('导出配置功能开发中...')
|
break
|
case 'test':
|
// 测试连接逻辑
|
try {
|
const response = await deviceGroupApi.healthCheck({ groupId: row.id || row.groupId })
|
ElMessage.success('连接测试成功')
|
} catch (error) {
|
ElMessage.error('连接测试失败: ' + (error.response?.data?.message || error.message))
|
}
|
break
|
case 'plc-report':
|
await triggerGroupPlcReport(row)
|
break
|
case 'delete':
|
await ElMessageBox.confirm('确定要删除该设备组吗?', '删除确认', {
|
confirmButtonText: '确定删除',
|
cancelButtonText: '取消',
|
type: 'warning'
|
})
|
await deviceGroupApi.delete({ groupId: row.id || row.groupId })
|
ElMessage.success('设备组删除成功')
|
loadGroupList()
|
emit('refresh-statistics')
|
break
|
}
|
}
|
|
const handleSizeChange = (size) => {
|
pagination.size = size
|
pagination.page = 1
|
loadGroupList()
|
}
|
|
const handleCurrentChange = (page) => {
|
pagination.page = page
|
loadGroupList()
|
}
|
|
// 工具函数
|
const getGroupTypeTag = (type) => {
|
const typeMap = {
|
'设备组': 'primary',
|
'管理组': 'success',
|
'监控组': 'warning',
|
'维护组': 'info'
|
}
|
return typeMap[type] || 'info'
|
}
|
|
const getGroupStatusTag = (status) => {
|
const statusMap = {
|
'ENABLED': 'success',
|
'DISABLED': 'info'
|
}
|
return statusMap[status] || 'info'
|
}
|
|
const getGroupStatusText = (status) => {
|
const statusMap = {
|
'ENABLED': '启用',
|
'DISABLED': '禁用'
|
}
|
return statusMap[status] || status
|
}
|
|
const getDeviceStatusTag = (status) => {
|
const statusMap = {
|
'ONLINE': 'success',
|
'OFFLINE': 'info',
|
'MAINTENANCE': 'warning',
|
'DISABLED': 'danger'
|
}
|
return statusMap[status] || 'info'
|
}
|
|
const getDeviceStatusText = (status) => {
|
const statusMap = {
|
'ONLINE': '在线',
|
'OFFLINE': '离线',
|
'MAINTENANCE': '维护中',
|
'DISABLED': '禁用'
|
}
|
return statusMap[status] || status
|
}
|
|
const getEnableRate = () => {
|
if (!currentGroup.value || !currentGroup.value.deviceCount) return 0
|
return Math.round((currentGroup.value.onlineDeviceCount / currentGroup.value.deviceCount) * 100)
|
}
|
|
// 暴露方法
|
const refresh = () => {
|
loadGroupList()
|
}
|
|
defineExpose({
|
refresh
|
})
|
|
// 组件挂载时加载数据
|
onMounted(() => {
|
loadGroupList()
|
})
|
</script>
|
|
<style scoped>
|
.device-group-list {
|
padding: 20px;
|
}
|
|
.search-section {
|
margin-bottom: 20px;
|
padding: 16px;
|
background-color: #f5f7fa;
|
border-radius: 8px;
|
}
|
|
.batch-operation {
|
margin-bottom: 16px;
|
padding: 16px;
|
background-color: #e6f7ff;
|
border: 1px solid #91d5ff;
|
border-radius: 8px;
|
}
|
|
.batch-buttons {
|
margin-top: 12px;
|
display: flex;
|
gap: 8px;
|
}
|
|
.table-section {
|
margin-bottom: 20px;
|
}
|
|
.pagination-section {
|
display: flex;
|
justify-content: center;
|
}
|
|
.device-management {
|
padding: 20px 0;
|
}
|
|
.dialog-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 20px;
|
padding-bottom: 16px;
|
border-bottom: 1px solid #ebeef5;
|
}
|
|
.device-stats {
|
display: flex;
|
gap: 40px;
|
}
|
|
.dialog-buttons {
|
display: flex;
|
gap: 12px;
|
}
|
|
.statistics-content {
|
padding: 20px 0;
|
}
|
|
.chart-container {
|
border: 1px solid #ebeef5;
|
border-radius: 8px;
|
}
|
|
:deep(.el-table .cell) {
|
white-space: nowrap;
|
}
|
|
:deep(.el-dropdown-menu__item.is-divided) {
|
border-top: 1px solid #ebeef5;
|
margin-top: 6px;
|
padding-top: 10px;
|
}
|
|
:deep(.el-statistic .el-statistic__title) {
|
font-weight: normal;
|
color: #666;
|
}
|
|
:deep(.el-statistic .el-statistic__content) {
|
font-size: 24px;
|
font-weight: bold;
|
color: #409eff;
|
}
|
</style>
|