<template>
|
<div class="device-config-list">
|
<!-- 搜索和筛选区域 -->
|
<div class="search-section">
|
<el-form :model="searchForm" :inline="true" class="search-form">
|
<el-form-item label="设备类型">
|
<el-select v-model="searchForm.deviceType" placeholder="选择设备类型" clearable :loading="deviceTypesLoading">
|
<el-option
|
v-for="type in deviceTypes"
|
:key="type"
|
:label="type"
|
:value="type"
|
/>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="设备状态">
|
<el-select v-model="searchForm.deviceStatus" placeholder="选择设备状态" clearable>
|
<el-option label="在线" value="ONLINE" />
|
<el-option label="离线" value="OFFLINE" />
|
<el-option label="维护中" value="MAINTENANCE" />
|
<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="selectedDevices.length > 0">
|
<el-alert
|
:title="`已选择 ${selectedDevices.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 type="info" size="small" :loading="plcOperationLoading" @click="batchPlcRequest">批量PLC请求</el-button>
|
<el-button type="info" size="small" :loading="plcOperationLoading" @click="batchPlcReport">批量PLC汇报</el-button>
|
<el-button type="info" size="small" :loading="plcOperationLoading" @click="batchPlcReset">批量PLC重置</el-button>
|
<el-button size="small" @click="clearSelection">取消选择</el-button>
|
</div>
|
</div>
|
|
<!-- 设备列表 -->
|
<div class="table-section">
|
<el-table
|
ref="deviceTable"
|
v-loading="tableLoading"
|
:data="deviceList"
|
@selection-change="handleSelectionChange"
|
border
|
stripe
|
style="width: 100%"
|
>
|
<el-table-column type="selection" width="55" />
|
<el-table-column prop="deviceName" label="设备名称" min-width="120" />
|
<el-table-column prop="deviceCode" label="设备编码" width="120" />
|
<el-table-column prop="deviceType" label="设备类型" width="100">
|
<template #default="scope">
|
<el-tag :type="getDeviceTypeTag(scope.row.deviceType)">
|
{{ scope.row.deviceType }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="plcIp" label="PLC IP" width="130" />
|
<el-table-column prop="plcType" label="PLC类型" width="100" />
|
<el-table-column prop="moduleName" label="模块名称" min-width="120" />
|
<el-table-column prop="isPrimary" label="主控设备" width="100" align="center">
|
<template #default="scope">
|
<el-tag v-if="scope.row.isPrimary" type="success" size="small">主控</el-tag>
|
<span v-else>-</span>
|
</template>
|
</el-table-column>
|
<el-table-column prop="deviceStatus" label="设备状态" width="100">
|
<template #default="scope">
|
<el-tag :type="getDeviceStatusTag(scope.row.deviceStatus)" size="small">
|
{{ getDeviceStatusText(scope.row.deviceStatus) }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="enabled" label="启用状态" width="100">
|
<template #default="scope">
|
<el-switch
|
v-model="scope.row.enabled"
|
:disabled="scope.row.isPrimary"
|
@change="handleStatusChange(scope.row)"
|
/>
|
</template>
|
</el-table-column>
|
<el-table-column prop="lastHeartbeat" label="最后心跳" width="150">
|
<template #default="scope">
|
{{ formatDateTime(scope.row.lastHeartbeat) }}
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="200" fixed="right">
|
<template #default="scope">
|
<el-button type="primary" size="small" @click="editDevice(scope.row)">
|
编辑
|
</el-button>
|
<el-button type="warning" size="small" :loading="plcOperationLoading" @click="handleSinglePlcRequest(scope.row)">
|
PLC请求
|
</el-button>
|
<el-button type="success" size="small" @click="healthCheck(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" :disabled="scope.row.isPrimary">复制配置</el-dropdown-item>
|
<el-dropdown-item command="reset">重置设备</el-dropdown-item>
|
<el-dropdown-item command="plc-report">PLC汇报</el-dropdown-item>
|
<el-dropdown-item command="plc-reset">PLC重置</el-dropdown-item>
|
<el-dropdown-item command="delete" divided :disabled="scope.row.isPrimary">
|
删除设备
|
</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>
|
</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 { deviceConfigApi, devicePlcApi } from '@/api/device/deviceManagement'
|
|
// 响应式数据
|
const deviceTable = ref(null)
|
const tableLoading = ref(false)
|
const deviceList = ref([])
|
const selectedDevices = ref([])
|
const plcOperationLoading = ref(false)
|
|
// 设备类型列表
|
const deviceTypes = ref([])
|
const deviceTypesLoading = ref(false)
|
|
// 搜索表单
|
const searchForm = reactive({
|
deviceType: '',
|
deviceStatus: '',
|
keyword: ''
|
})
|
|
// 分页信息
|
const pagination = reactive({
|
page: 1,
|
size: 10,
|
total: 0
|
})
|
|
// 事件定义
|
const emit = defineEmits(['device-selected', 'refresh-statistics'])
|
|
// 方法定义
|
// 加载设备类型列表
|
const loadDeviceTypes = async () => {
|
if (deviceTypes.value.length > 0) {
|
// 如果已经加载过,不再重复加载
|
return
|
}
|
// 所有支持的设备类型(确保包含所有有配置组件的类型)
|
const supportedTypes = ['大车设备', '大理片笼', '卧转立扫码设备', '卧转立设备']
|
|
try {
|
deviceTypesLoading.value = true
|
const res = await deviceConfigApi.getDeviceTypes()
|
if (res?.data && Array.isArray(res.data)) {
|
// 合并数据库中的类型和支持的类型,去重并排序
|
const dbTypes = res.data
|
const allTypes = [...new Set([...supportedTypes, ...dbTypes])].sort()
|
deviceTypes.value = allTypes
|
} else {
|
// 如果API返回失败,使用默认类型
|
deviceTypes.value = supportedTypes
|
console.warn('获取设备类型列表失败,使用默认类型')
|
}
|
} catch (error) {
|
console.error('加载设备类型列表失败:', error)
|
// 失败时使用默认类型
|
deviceTypes.value = supportedTypes
|
} finally {
|
deviceTypesLoading.value = false
|
}
|
}
|
|
const loadDeviceList = async () => {
|
try {
|
tableLoading.value = true
|
const params = {
|
pageNum: pagination.page,
|
pageSize: pagination.size,
|
deviceType: searchForm.deviceType || undefined,
|
deviceStatus: searchForm.deviceStatus || undefined,
|
keyword: searchForm.keyword?.trim() || undefined
|
}
|
|
const response = await deviceConfigApi.getList(params)
|
// MyBatis-Plus Page 对象结构:{ records: [], total: 0 }
|
if (response && response.data) {
|
deviceList.value = response.data.records || response.data.content || response.data.list || []
|
pagination.total = response.data.total || response.data.totalElements || 0
|
} else {
|
deviceList.value = []
|
pagination.total = 0
|
}
|
} catch (error) {
|
console.error('加载设备列表失败:', error)
|
ElMessage.error('加载设备列表失败: ' + (error.response?.data?.message || error.message))
|
deviceList.value = []
|
pagination.total = 0
|
} finally {
|
tableLoading.value = false
|
}
|
}
|
|
const handleSearch = () => {
|
pagination.page = 1
|
loadDeviceList()
|
}
|
|
const resetSearch = () => {
|
searchForm.deviceType = ''
|
searchForm.deviceStatus = ''
|
searchForm.keyword = ''
|
pagination.page = 1
|
loadDeviceList()
|
}
|
|
const handleSelectionChange = (selection) => {
|
selectedDevices.value = selection
|
}
|
|
const clearSelection = () => {
|
deviceTable.value?.clearSelection()
|
selectedDevices.value = []
|
}
|
|
const getSelectedDeviceIds = () => selectedDevices.value.map(item => item.id || item.deviceId)
|
|
const plcOperationLabelMap = {
|
request: 'PLC请求',
|
report: 'PLC汇报',
|
reset: 'PLC重置'
|
}
|
|
const executePlcOperation = async (deviceIds, operation) => {
|
const ids = deviceIds?.filter(Boolean) || []
|
if (ids.length === 0) {
|
ElMessage.warning('请先选择设备')
|
return
|
}
|
plcOperationLoading.value = true
|
try {
|
let response
|
switch (operation) {
|
case 'request':
|
response = await devicePlcApi.triggerRequests(ids)
|
break
|
case 'report':
|
response = await devicePlcApi.triggerReports(ids)
|
break
|
case 'reset':
|
response = await devicePlcApi.resetDevices(ids)
|
break
|
default:
|
throw new Error('未知的PLC操作类型')
|
}
|
const results = response?.data || []
|
const successCount = results.filter(item => item.success).length
|
const label = plcOperationLabelMap[operation] || 'PLC操作'
|
if (results.length === 0) {
|
ElMessage.warning(`${label}未返回结果`)
|
} else if (successCount === results.length) {
|
ElMessage.success(`${label}成功(${successCount}/${results.length})`)
|
} else {
|
ElMessage.warning(`${label}部分成功(${successCount}/${results.length})`)
|
}
|
} catch (error) {
|
console.error('执行PLC操作失败:', error)
|
const label = plcOperationLabelMap[operation] || 'PLC操作'
|
ElMessage.error(`${label}失败:${error.response?.data?.message || error.message}`)
|
} finally {
|
plcOperationLoading.value = false
|
}
|
}
|
|
const handleSinglePlcRequest = (row) => executePlcOperation([row.id || row.deviceId], 'request')
|
const handleSinglePlcReport = (row) => executePlcOperation([row.id || row.deviceId], 'report')
|
const handleSinglePlcReset = (row) => executePlcOperation([row.id || row.deviceId], 'reset')
|
|
const batchPlcRequest = () => executePlcOperation(getSelectedDeviceIds(), 'request')
|
const batchPlcReport = () => executePlcOperation(getSelectedDeviceIds(), 'report')
|
const batchPlcReset = () => executePlcOperation(getSelectedDeviceIds(), 'reset')
|
|
const handleStatusChange = async (row) => {
|
try {
|
if (row.enabled) {
|
await deviceConfigApi.enable(row.id || row.deviceId)
|
ElMessage.success('设备启用成功')
|
} else {
|
await deviceConfigApi.disable(row.id || row.deviceId)
|
ElMessage.success('设备禁用成功')
|
}
|
emit('refresh-statistics')
|
loadDeviceList() // 刷新列表
|
} catch (error) {
|
console.error('更新设备状态失败:', error)
|
row.enabled = !row.enabled // 恢复状态
|
ElMessage.error('更新设备状态失败: ' + (error.response?.data?.message || error.message))
|
}
|
}
|
|
const batchEnable = async () => {
|
try {
|
const deviceIds = selectedDevices.value.map(item => item.id || item.deviceId)
|
await deviceConfigApi.batchEnable(deviceIds)
|
ElMessage.success(`成功启用 ${deviceIds.length} 个设备`)
|
clearSelection()
|
loadDeviceList()
|
emit('refresh-statistics')
|
} catch (error) {
|
console.error('批量启用失败:', error)
|
ElMessage.error('批量启用失败: ' + (error.response?.data?.message || error.message))
|
}
|
}
|
|
const batchDisable = async () => {
|
try {
|
const deviceIds = selectedDevices.value.map(item => item.id || item.deviceId)
|
await deviceConfigApi.batchDisable(deviceIds)
|
ElMessage.success(`成功禁用 ${deviceIds.length} 个设备`)
|
clearSelection()
|
loadDeviceList()
|
emit('refresh-statistics')
|
} catch (error) {
|
console.error('批量禁用失败:', error)
|
ElMessage.error('批量禁用失败: ' + (error.response?.data?.message || error.message))
|
}
|
}
|
|
const batchDelete = async () => {
|
try {
|
await ElMessageBox.confirm(
|
`确定要删除选中的 ${selectedDevices.value.length} 个设备吗?此操作不可恢复!`,
|
'批量删除确认',
|
{
|
confirmButtonText: '确定删除',
|
cancelButtonText: '取消',
|
type: 'warning'
|
}
|
)
|
|
const deviceIds = selectedDevices.value.map(item => item.id || item.deviceId)
|
// 逐个删除
|
for (const deviceId of deviceIds) {
|
await deviceConfigApi.delete(deviceId)
|
}
|
ElMessage.success(`成功删除 ${deviceIds.length} 个设备`)
|
clearSelection()
|
loadDeviceList()
|
emit('refresh-statistics')
|
} catch (error) {
|
if (error !== 'cancel') {
|
console.error('批量删除失败:', error)
|
ElMessage.error('批量删除失败')
|
}
|
}
|
}
|
|
const editDevice = (row) => {
|
emit('device-selected', row)
|
}
|
|
const healthCheck = async (row) => {
|
try {
|
const response = await deviceConfigApi.healthCheck(row.id || row.deviceId)
|
if (response && response.data) {
|
ElMessage.success(`设备健康检查完成,状态:${response.data.status || '正常'}`)
|
} else {
|
ElMessage.success('设备健康检查完成')
|
}
|
} catch (error) {
|
console.error('健康检查失败:', error)
|
ElMessage.error('健康检查失败: ' + (error.response?.data?.message || error.message))
|
}
|
}
|
|
const handleCommand = async (command, row) => {
|
switch (command) {
|
case 'view':
|
// 查看详情逻辑
|
ElMessage.info('查看详情功能开发中...')
|
break
|
case 'copy':
|
// 复制配置逻辑
|
ElMessage.info('复制配置功能开发中...')
|
break
|
case 'reset':
|
// 重置设备逻辑
|
await ElMessageBox.confirm('确定要重置设备配置吗?', '重置确认')
|
ElMessage.success('设备重置成功')
|
break
|
case 'plc-report':
|
await executePlcOperation([row.id || row.deviceId], 'report')
|
break
|
case 'plc-reset':
|
await executePlcOperation([row.id || row.deviceId], 'reset')
|
break
|
case 'delete':
|
if (row.isPrimary) {
|
ElMessage.warning('主控设备不能删除')
|
return
|
}
|
await ElMessageBox.confirm('确定要删除该设备吗?', '删除确认', {
|
confirmButtonText: '确定删除',
|
cancelButtonText: '取消',
|
type: 'warning'
|
})
|
await deviceConfigApi.delete(row.id || row.deviceId)
|
ElMessage.success('设备删除成功')
|
loadDeviceList()
|
emit('refresh-statistics')
|
break
|
}
|
}
|
|
const handleSizeChange = (size) => {
|
pagination.size = size
|
pagination.page = 1
|
loadDeviceList()
|
}
|
|
const handleCurrentChange = (page) => {
|
pagination.page = page
|
loadDeviceList()
|
}
|
|
// 工具函数
|
const getDeviceTypeTag = (type) => {
|
const typeMap = {
|
'大车设备': 'primary',
|
'大理片笼': 'success',
|
'卧式缓存': 'warning'
|
}
|
return typeMap[type] || 'info'
|
}
|
|
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 formatDateTime = (dateTime) => {
|
if (!dateTime) return '-'
|
return new Date(dateTime).toLocaleString()
|
}
|
|
// 暴露方法
|
const refresh = () => {
|
loadDeviceList()
|
}
|
|
defineExpose({
|
refresh
|
})
|
|
// 组件挂载时加载数据
|
onMounted(() => {
|
loadDeviceTypes()
|
loadDeviceList()
|
})
|
</script>
|
|
<style scoped>
|
.device-config-list {
|
padding: 20px;
|
}
|
|
.search-section {
|
margin-bottom: 20px;
|
padding: 16px;
|
background-color: #f5f7fa;
|
border-radius: 8px;
|
}
|
|
.search-form {
|
display: flex;
|
align-items: center;
|
flex-wrap: wrap;
|
}
|
|
.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;
|
}
|
|
: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;
|
}
|
</style>
|