iot优化
parent
a69e27fcf9
commit
d94b5f3851
|
|
@ -71,6 +71,29 @@ export interface IotDeviceMessageSendReqVO {
|
|||
params?: any // 请求参数
|
||||
}
|
||||
|
||||
// IoT 设备消息拉取 Request VO
|
||||
export interface IotDeviceMessagePullReqVO {
|
||||
deviceId: number // 设备 ID
|
||||
limit?: number // 拉取数量限制(默认 10,最大 100)
|
||||
}
|
||||
|
||||
// IoT 设备消息拉取 Response VO
|
||||
export interface IotDeviceMessagePullRespVO {
|
||||
id: number // 离线消息记录 ID
|
||||
messageId: string // 消息 ID
|
||||
method: string // 消息方法
|
||||
message: any // 完整的消息对象
|
||||
retryCount: number // 重试次数
|
||||
createTime: Date // 创建时间
|
||||
expireTime: Date // 过期时间
|
||||
}
|
||||
|
||||
// IoT 设备消息确认 Request VO
|
||||
export interface IotDeviceMessageAckReqVO {
|
||||
messageId: number // 消息 ID(离线消息表的主键 ID)
|
||||
deviceId: number // 设备 ID
|
||||
}
|
||||
|
||||
// 设备 API
|
||||
export const DeviceApi = {
|
||||
// 查询设备分页
|
||||
|
|
@ -161,5 +184,32 @@ export const DeviceApi = {
|
|||
// 发送设备消息
|
||||
sendDeviceMessage: async (params: IotDeviceMessageSendReqVO) => {
|
||||
return await request.post({ url: `/iot/device/message/send`, data: params })
|
||||
},
|
||||
|
||||
// 拉取待处理消息(Pull 模式)
|
||||
pullDeviceMessages: async (params: IotDeviceMessagePullReqVO) => {
|
||||
return await request.get({ url: `/iot/device/message/pull`, params })
|
||||
},
|
||||
|
||||
// 确认消息已处理(Pull 模式)
|
||||
confirmDeviceMessage: async (data: IotDeviceMessageAckReqVO) => {
|
||||
return await request.post({ url: `/iot/device/message/ack`, data })
|
||||
},
|
||||
|
||||
// 获取设备离线消息列表(管理后台)
|
||||
getPullMessageList: async (deviceId: number, status?: number) => {
|
||||
return await request.get({
|
||||
url: `/iot/device/message/pull-list`,
|
||||
params: { deviceId, status }
|
||||
})
|
||||
},
|
||||
|
||||
// 删除离线消息(管理后台)
|
||||
deletePullMessage: async (messageId: number, deviceId: number) => {
|
||||
return await request.delete({
|
||||
url: `/iot/device/message/pull-delete`,
|
||||
params: { messageId, deviceId }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import request from '@/config/axios'
|
||||
|
||||
export interface OtaConfig {
|
||||
allowOfflinePush: boolean // 是否允许离线推送
|
||||
}
|
||||
|
||||
// 获取 OTA 配置
|
||||
export const getOtaConfig = async () => {
|
||||
return await request.get<OtaConfig>({ url: `/iot/ota/config/get` })
|
||||
}
|
||||
|
||||
// 更新 OTA 配置
|
||||
export const updateOtaConfig = async (data: OtaConfig) => {
|
||||
return await request.put({ url: `/iot/ota/config/update`, data })
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import request from '@/config/axios'
|
||||
|
||||
export interface OtaTaskStatistics {
|
||||
// PUSH 模式统计
|
||||
pushOnlineTotalCount: number // PUSH 在线推送总数
|
||||
pushOnlineSuccessCount: number // PUSH 在线推送成功数
|
||||
|
||||
// PULL 模式统计
|
||||
pullOfflineCount: number // PULL 离线消息总数
|
||||
pullDeliveredCount: number // PULL 已送达数量(设备已拉取)
|
||||
pullTodayCount: number // 今日新增 PULL 消息数
|
||||
|
||||
// 设备统计
|
||||
totalDeviceCount: number // 总设备数
|
||||
onlineDeviceCount: number // 在线设备数
|
||||
offlineDeviceCount: number // 离线设备数
|
||||
|
||||
// 任务完成率
|
||||
totalTaskCount: number // 总任务数
|
||||
completedTaskCount: number // 已完成任务数(成功+失败)
|
||||
}
|
||||
|
||||
// 获取 OTA 任务统计信息
|
||||
export const getOtaTaskStatistics = async (params?: {
|
||||
startTime?: Date
|
||||
endTime?: Date
|
||||
}) => {
|
||||
return await request.get<OtaTaskStatistics>({ url: `/iot/ota/task/statistics`, params })
|
||||
}
|
||||
|
||||
// 导出 API
|
||||
export const OtaTaskStatisticsApi = {
|
||||
getOtaTaskStatistics
|
||||
}
|
||||
|
|
@ -246,5 +246,6 @@ export enum DICT_TYPE {
|
|||
IOT_ALERT_RECEIVE_TYPE = 'iot_alert_receive_type', // IoT 告警接收类型
|
||||
IOT_OTA_TASK_DEVICE_SCOPE = 'iot_ota_task_device_scope', // IoT OTA任务设备范围
|
||||
IOT_OTA_TASK_STATUS = 'iot_ota_task_status', // IoT OTA 任务状态
|
||||
IOT_OTA_TASK_RECORD_STATUS = 'iot_ota_task_record_status' // IoT OTA 记录状态
|
||||
IOT_OTA_TASK_RECORD_STATUS = 'iot_ota_task_record_status', // IoT OTA 记录状态
|
||||
IOT_STATUS_OPTIONS = 'iot_status_options' //消息状态
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@
|
|||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="DeviceName" prop="deviceName">
|
||||
<el-form-item label="设备名称" prop="deviceName">
|
||||
<el-input
|
||||
v-model="formData.deviceName"
|
||||
placeholder="请输入 DeviceName"
|
||||
placeholder="请输入设备名称"
|
||||
:disabled="formType === 'update'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@
|
|||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="DeviceName" prop="deviceName">
|
||||
<el-form-item label="设备名称" prop="deviceName">
|
||||
<el-input
|
||||
v-model="queryParams.deviceName"
|
||||
placeholder="请输入 DeviceName"
|
||||
placeholder="请输入设备名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,232 @@
|
|||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 筛选栏 -->
|
||||
<el-form :inline="true" class="mb-4">
|
||||
<el-form-item label="消息状态">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
@change="getList"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_STATUS_OPTIONS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="getList" :icon="Search">查询</el-button>
|
||||
<el-button @click="resetQuery" :icon="Refresh">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table v-loading="loading" :data="list" border>
|
||||
<el-table-column label="消息 ID" prop="messageId" width="200" show-overflow-tooltip />
|
||||
<el-table-column label="消息方法" prop="method" width="180" show-overflow-tooltip />
|
||||
<el-table-column label="状态" prop="status" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.IOT_STATUS_OPTIONS" :value="row.message?.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="重试次数" prop="retryCount" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.retryCount > 0" type="warning">{{ row.retryCount }}</el-tag>
|
||||
<span v-else>{{ row.retryCount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="消息内容" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-text line-clamp="2" style="max-width: 100%">
|
||||
{{ formatMessageContent(row.message) }}
|
||||
</el-text>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" prop="createTime" width="160" :formatter="dateFormatter" />
|
||||
<el-table-column label="过期时间" prop="expireTime" width="160" :formatter="dateFormatter">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-danger': isExpiringSoon(row.expireTime) }">
|
||||
{{ dateFormatter(row, '', '', row.expireTime) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleViewDetail(row)"
|
||||
:icon="View"
|
||||
>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
:icon="Delete"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 消息详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="离线消息详情"
|
||||
width="800px"
|
||||
append-to-body
|
||||
>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="消息 ID">
|
||||
{{ currentMessage?.messageId }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="消息方法">
|
||||
{{ currentMessage?.method }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<dict-tag :type="DICT_TYPE.IOT_STATUS_OPTIONS" :value="currentMessage?.message?.status" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="重试次数">
|
||||
{{ currentMessage?.retryCount }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">
|
||||
{{ dateFormatter(currentMessage, '', '', currentMessage?.createTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="过期时间" :span="2">
|
||||
{{ dateFormatter(currentMessage, '', '', currentMessage?.expireTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="消息内容" :span="2">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:value="formatMessageJson(currentMessage?.message)"
|
||||
:rows="10"
|
||||
readonly
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Search, Refresh, View, Delete } from '@element-plus/icons-vue'
|
||||
import { DeviceApi, type IotDeviceMessagePullRespVO } from '@/api/iot/device/device'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number
|
||||
}>()
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
status: undefined as number | undefined
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const loading = ref(false)
|
||||
const list = ref<IotDeviceMessagePullRespVO[]>([])
|
||||
|
||||
// 消息详情
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentMessage = ref<IotDeviceMessagePullRespVO>()
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
// 如果 deviceId 不存在,不执行查询
|
||||
if (!props.deviceId) {
|
||||
console.warn('deviceId 为空,跳过查询离线消息列表')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await DeviceApi.getPullMessageList(props.deviceId, queryParams.status)
|
||||
list.value = data
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置查询 */
|
||||
const resetQuery = () => {
|
||||
queryParams.status = undefined
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
const handleViewDetail = (row: IotDeviceMessagePullRespVO) => {
|
||||
currentMessage.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
/** 删除消息 */
|
||||
const handleDelete = async (row: IotDeviceMessagePullRespVO) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该离线消息吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await DeviceApi.deletePullMessage(row.id, props.deviceId)
|
||||
ElMessage.success('删除成功')
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 格式化消息内容(简略显示) */
|
||||
const formatMessageContent = (message: any) => {
|
||||
if (!message) return '-'
|
||||
const params = message.params || message.data
|
||||
return JSON.stringify(params || {})
|
||||
}
|
||||
|
||||
/** 格式化消息 JSON(完整显示) */
|
||||
const formatMessageJson = (message: any) => {
|
||||
if (!message) return ''
|
||||
return JSON.stringify(message, null, 2)
|
||||
}
|
||||
|
||||
/** 判断是否即将过期(24小时内) */
|
||||
const isExpiringSoon = (expireTime: Date) => {
|
||||
if (!expireTime) return false
|
||||
const now = new Date().getTime()
|
||||
const expire = new Date(expireTime).getTime()
|
||||
const diff = expire - now
|
||||
return diff > 0 && diff < 24 * 60 * 60 * 1000 // 24小时
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
/** 监听 deviceId 变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(val) => {
|
||||
if (val) {
|
||||
getList()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 暴露刷新方法给父组件
|
||||
defineExpose({
|
||||
refresh: getList
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-danger {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -131,6 +131,7 @@ const getList = async () => {
|
|||
if (!props.deviceId) return
|
||||
loading.value = true
|
||||
try {
|
||||
queryParams.deviceId = props.deviceId
|
||||
const data = await DeviceApi.getDeviceMessagePairPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.length
|
||||
|
|
@ -139,6 +140,17 @@ const getList = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
/** 监听 deviceId 变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(val) => {
|
||||
if (val) {
|
||||
queryParams.deviceId = val
|
||||
getList()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
|
|
|
|||
|
|
@ -171,6 +171,9 @@ const queryFormRef = ref() // 搜索的表单
|
|||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
if (!props.deviceId) {
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
|
|
@ -185,6 +188,16 @@ const getList = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
/** 监听 deviceId 变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(val) => {
|
||||
if (val) {
|
||||
getList()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/** 前端筛选数据 */
|
||||
const handleFilter = () => {
|
||||
if (!queryParams.keyword.trim()) {
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ const getList = async () => {
|
|||
if (!props.deviceId) return
|
||||
loading.value = true
|
||||
try {
|
||||
queryParams.deviceId = props.deviceId
|
||||
const data = await DeviceApi.getDeviceMessagePairPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.length
|
||||
|
|
@ -154,6 +155,17 @@ const getList = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
/** 监听 deviceId 变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(val) => {
|
||||
if (val) {
|
||||
queryParams.deviceId = val
|
||||
getList()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@
|
|||
<el-tab-pane label="设备消息" name="log">
|
||||
<DeviceDetailsMessage v-if="activeTab === 'log'" :device-id="device.id" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="离线消息" name="pullMessage">
|
||||
<DeviceDetailsPullMessage v-if="activeTab === 'pullMessage'" :device-id="device.id" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="模拟设备" name="simulator">
|
||||
<DeviceDetailsSimulator
|
||||
v-if="activeTab === 'simulator'"
|
||||
|
|
@ -48,6 +51,7 @@ import DeviceDetailsHeader from './DeviceDetailsHeader.vue'
|
|||
import DeviceDetailsInfo from './DeviceDetailsInfo.vue'
|
||||
import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue'
|
||||
import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
|
||||
import DeviceDetailsPullMessage from './DeviceDetailsPullMessage.vue'
|
||||
import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue'
|
||||
import DeviceDetailConfig from './DeviceDetailConfig.vue'
|
||||
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@
|
|||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="DeviceName" prop="deviceName">
|
||||
<el-form-item label="设备名称" prop="deviceName">
|
||||
<el-input
|
||||
v-model="queryParams.deviceName"
|
||||
placeholder="请输入 DeviceName"
|
||||
placeholder="请输入设备名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
|
|
@ -519,7 +519,7 @@ onMounted(async () => {
|
|||
if (productId) {
|
||||
queryParams.productId = Number(productId)
|
||||
}
|
||||
|
||||
|
||||
getList()
|
||||
|
||||
// 获取产品列表
|
||||
|
|
|
|||
|
|
@ -0,0 +1,298 @@
|
|||
<template>
|
||||
<ContentWrap title="OTA 升级监控" class="ota-monitor-card">
|
||||
<el-row :gutter="16">
|
||||
<!-- PUSH 成功率 -->
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-header">
|
||||
<Icon icon="ep:upload" class="stat-icon text-blue-500" :size="32" />
|
||||
<span class="stat-title">PUSH 成功率</span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value" :class="getPushRateClass(pushSuccessRate)">
|
||||
{{ loading ? '-' : pushSuccessRate.toFixed(1) }}%
|
||||
</div>
|
||||
<div class="stat-detail">
|
||||
成功 {{ pushOnlineSuccessCount }} / 总数 {{ pushOnlineTotalCount }}
|
||||
</div>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="pushSuccessRate"
|
||||
:color="getPushRateColor(pushSuccessRate)"
|
||||
:show-text="false"
|
||||
/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- PULL 消息数量 -->
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-header">
|
||||
<Icon icon="ep:download" class="stat-icon text-orange-500" :size="32" />
|
||||
<span class="stat-title">PULL 消息数量</span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value text-orange-500">
|
||||
{{ loading ? '-' : pullOfflineCount }}
|
||||
</div>
|
||||
<div class="stat-detail">
|
||||
已送达 {{ pullDeliveredCount }} / 待拉取 {{ pullPendingCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-trend">
|
||||
<span class="trend-label">今日新增:</span>
|
||||
<span class="trend-value">+{{ pullTodayCount }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 离线设备比例 -->
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-header">
|
||||
<Icon icon="ep:connection" class="stat-icon text-purple-500" :size="32" />
|
||||
<span class="stat-title">离线设备比例</span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value" :class="getOfflineRateClass(offlineRate)">
|
||||
{{ loading ? '-' : offlineRate.toFixed(1) }}%
|
||||
</div>
|
||||
<div class="stat-detail">
|
||||
离线 {{ offlineDeviceCount }} / 总数 {{ totalDeviceCount }}
|
||||
</div>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="offlineRate"
|
||||
:color="getOfflineRateColor(offlineRate)"
|
||||
:show-text="false"
|
||||
/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 升级任务完成率 -->
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-header">
|
||||
<Icon icon="ep:circle-check" class="stat-icon text-green-500" :size="32" />
|
||||
<span class="stat-title">任务完成率</span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value text-green-500">
|
||||
{{ loading ? '-' : completionRate.toFixed(1) }}%
|
||||
</div>
|
||||
<div class="stat-detail">
|
||||
完成 {{ completedTaskCount }} / 总数 {{ totalTaskCount }}
|
||||
</div>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="completionRate"
|
||||
color="#67C23A"
|
||||
:show-text="false"
|
||||
/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 告警提示 -->
|
||||
<el-row v-if="hasAlerts" class="mt-4">
|
||||
<el-col :span="24">
|
||||
<el-alert
|
||||
v-if="pushSuccessRate < 90 && pushSuccessRate >= 0"
|
||||
:title="`PUSH 成功率过低 (${pushSuccessRate.toFixed(1)}%),建议检查网络连接和设备状态`"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<el-alert
|
||||
v-if="offlineRate > 50"
|
||||
:title="`离线设备比例过高 (${offlineRate.toFixed(1)}%),建议检查设备连接状况`"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mt-2"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { OtaTaskStatisticsApi, type OtaTaskStatistics } from '@/api/iot/ota/statistics'
|
||||
|
||||
/** OTA 监控卡片 */
|
||||
defineOptions({ name: 'OtaMonitorCard' })
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
// PUSH 统计
|
||||
const pushOnlineTotalCount = ref(0)
|
||||
const pushOnlineSuccessCount = ref(0)
|
||||
const pushSuccessRate = ref(0)
|
||||
|
||||
// PULL 统计
|
||||
const pullOfflineCount = ref(0)
|
||||
const pullDeliveredCount = ref(0)
|
||||
const pullPendingCount = ref(0)
|
||||
const pullTodayCount = ref(0)
|
||||
|
||||
// 设备统计
|
||||
const totalDeviceCount = ref(0)
|
||||
const offlineDeviceCount = ref(0)
|
||||
const offlineRate = ref(0)
|
||||
|
||||
// 任务统计
|
||||
const totalTaskCount = ref(0)
|
||||
const completedTaskCount = ref(0)
|
||||
const completionRate = ref(0)
|
||||
|
||||
/** 是否有告警 */
|
||||
const hasAlerts = computed(() => {
|
||||
return pushSuccessRate.value < 90 || offlineRate.value > 50
|
||||
})
|
||||
|
||||
/** 获取 PUSH 成功率颜色类 */
|
||||
const getPushRateClass = (rate: number) => {
|
||||
if (rate >= 95) return 'text-green-500'
|
||||
if (rate >= 90) return 'text-orange-500'
|
||||
return 'text-red-500'
|
||||
}
|
||||
|
||||
/** 获取 PUSH 成功率颜色 */
|
||||
const getPushRateColor = (rate: number) => {
|
||||
if (rate >= 95) return '#67C23A'
|
||||
if (rate >= 90) return '#E6A23C'
|
||||
return '#F56C6C'
|
||||
}
|
||||
|
||||
/** 获取离线率颜色类 */
|
||||
const getOfflineRateClass = (rate: number) => {
|
||||
if (rate < 30) return 'text-green-500'
|
||||
if (rate < 50) return 'text-orange-500'
|
||||
return 'text-red-500'
|
||||
}
|
||||
|
||||
/** 获取离线率颜色 */
|
||||
const getOfflineRateColor = (rate: number) => {
|
||||
if (rate < 30) return '#67C23A'
|
||||
if (rate < 50) return '#E6A23C'
|
||||
return '#F56C6C'
|
||||
}
|
||||
|
||||
/** 获取统计数据 */
|
||||
const getStatistics = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await OtaTaskStatisticsApi.getOtaTaskStatistics()
|
||||
|
||||
// PUSH 统计
|
||||
pushOnlineTotalCount.value = data.pushOnlineTotalCount || 0
|
||||
pushOnlineSuccessCount.value = data.pushOnlineSuccessCount || 0
|
||||
pushSuccessRate.value = pushOnlineTotalCount.value > 0
|
||||
? (pushOnlineSuccessCount.value / pushOnlineTotalCount.value) * 100
|
||||
: 0
|
||||
|
||||
// PULL 统计
|
||||
pullOfflineCount.value = data.pullOfflineCount || 0
|
||||
pullDeliveredCount.value = data.pullDeliveredCount || 0
|
||||
pullPendingCount.value = pullOfflineCount.value - pullDeliveredCount.value
|
||||
pullTodayCount.value = data.pullTodayCount || 0
|
||||
|
||||
// 设备统计
|
||||
totalDeviceCount.value = data.totalDeviceCount || 0
|
||||
offlineDeviceCount.value = data.offlineDeviceCount || 0
|
||||
offlineRate.value = totalDeviceCount.value > 0
|
||||
? (offlineDeviceCount.value / totalDeviceCount.value) * 100
|
||||
: 0
|
||||
|
||||
// 任务统计
|
||||
totalTaskCount.value = data.totalTaskCount || 0
|
||||
completedTaskCount.value = data.completedTaskCount || 0
|
||||
completionRate.value = totalTaskCount.value > 0
|
||||
? (completedTaskCount.value / totalTaskCount.value) * 100
|
||||
: 0
|
||||
} catch (error) {
|
||||
console.error('获取 OTA 统计数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getStatistics()
|
||||
// 每 30 秒刷新一次
|
||||
setInterval(getStatistics, 30000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ota-monitor-card {
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
height: 160px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.stat-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-detail {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
.trend-label {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.trend-value {
|
||||
color: #67C23A;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -53,14 +53,21 @@
|
|||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 第三行:消息统计行 -->
|
||||
<!-- 第三行:OTA 监控面板 -->
|
||||
<el-row class="mb-4">
|
||||
<el-col :span="24">
|
||||
<OtaMonitorCard />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 第四行:消息统计行 -->
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<MessageTrendCard />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- TODO 第四行:地图 -->
|
||||
<!-- TODO 第五行:地图 -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Index">
|
||||
|
|
@ -69,6 +76,7 @@ import ComparisonCard from './components/ComparisonCard.vue'
|
|||
import DeviceCountCard from './components/DeviceCountCard.vue'
|
||||
import DeviceStateCountCard from './components/DeviceStateCountCard.vue'
|
||||
import MessageTrendCard from './components/MessageTrendCard.vue'
|
||||
import OtaMonitorCard from './components/OtaMonitorCard.vue'
|
||||
|
||||
/** IoT 首页 */
|
||||
defineOptions({ name: 'IoTHome' })
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
<el-button type="primary" @click="openTaskForm" v-hasPermi="['iot:ota-task:create']">
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
<el-button @click="openConfigDialog" v-hasPermi="['iot:ota-config:update']">
|
||||
<Icon icon="ep:setting" class="mr-5px" /> 配置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item class="float-right">
|
||||
<el-input
|
||||
|
|
@ -91,12 +94,36 @@
|
|||
|
||||
<!-- 任务详情弹窗 -->
|
||||
<OtaTaskDetail ref="taskDetailRef" @success="refresh" />
|
||||
|
||||
<!-- 配置对话框 -->
|
||||
<el-dialog v-model="configDialogVisible" title="OTA 升级配置" width="500px">
|
||||
<el-form :model="configForm" label-width="140px">
|
||||
<el-form-item label="允许离线推送">
|
||||
<el-switch
|
||||
v-model="configForm.allowOfflinePush"
|
||||
active-text="开启"
|
||||
inactive-text="关闭"
|
||||
/>
|
||||
<div class="text-xs text-gray-500 mt-2">
|
||||
开启后,离线设备也会接收升级指令并保存到离线消息表,设备上线后可主动拉取
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="configDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveConfig" :loading="configSaving">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { IoTOtaTaskApi, OtaTask } from '@/api/iot/ota/task'
|
||||
import * as IoTOtaConfigApi from '@/api/iot/ota/config'
|
||||
import type { OtaConfig } from '@/api/iot/ota/config'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { IoTOtaTaskStatusEnum } from '@/views/iot/utils/constants'
|
||||
import OtaTaskForm from './OtaTaskForm.vue'
|
||||
|
|
@ -126,6 +153,13 @@ const queryFormRef = ref() // 查询表单引用
|
|||
const taskFormRef = ref() // 任务表单引用
|
||||
const taskDetailRef = ref() // 任务详情引用
|
||||
|
||||
// 配置对话框
|
||||
const configDialogVisible = ref(false)
|
||||
const configSaving = ref(false)
|
||||
const configForm = reactive<OtaConfig>({
|
||||
allowOfflinePush: true
|
||||
})
|
||||
|
||||
/** 获取任务列表 */
|
||||
const getTaskList = async () => {
|
||||
taskLoading.value = true
|
||||
|
|
@ -180,6 +214,31 @@ const refresh = async () => {
|
|||
emit('success')
|
||||
}
|
||||
|
||||
/** 打开配置对话框 */
|
||||
const openConfigDialog = async () => {
|
||||
try {
|
||||
const config = await IoTOtaConfigApi.getOtaConfig()
|
||||
configForm.allowOfflinePush = config.allowOfflinePush
|
||||
configDialogVisible.value = true
|
||||
} catch (error) {
|
||||
console.error('获取配置失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存配置 */
|
||||
const handleSaveConfig = async () => {
|
||||
try {
|
||||
configSaving.value = true
|
||||
await IoTOtaConfigApi.updateOtaConfig(configForm)
|
||||
message.success('配置保存成功')
|
||||
configDialogVisible.value = false
|
||||
} catch (error) {
|
||||
console.error('保存配置失败', error)
|
||||
} finally {
|
||||
configSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getTaskList()
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@
|
|||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<div v-if="isModbusProtocol" class="form-tip">Modbus 协议自动使用 HEX 格式</div>
|
||||
<div v-if="isModbusProtocol" class="form-tip modbus-tip">Modbus 协议自动使用 HEX 格式</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="协议类型" prop="protocolType">
|
||||
<el-select v-model="formData.protocolType" placeholder="请选择协议类型" clearable @change="handleProtocolTypeChange">
|
||||
|
|
@ -184,7 +184,11 @@ const isModbusProtocol = computed(() => {
|
|||
const handleProtocolTypeChange = (value: string) => {
|
||||
if (value === 'MODBUS_RTU' || value === 'MODBUS_TCP') {
|
||||
// Modbus 协议自动设置数据格式为 HEX
|
||||
formData.value.codecType = 'HEX'
|
||||
// 查找已存在的 HEX 选项(忽略大小写)
|
||||
const options = getStrDictOptions(DICT_TYPE.IOT_CODEC_TYPE)
|
||||
const hexOption = options.find((item) => item.value.toUpperCase() === 'HEX')
|
||||
formData.value.codecType = hexOption ? hexOption.value : 'HEX' // 如果存在则使用存在的 options 值,否则使用 'HEX'
|
||||
|
||||
// 设置默认从站ID
|
||||
if (!formData.value.modbusSlaveId) {
|
||||
formData.value.modbusSlaveId = 1
|
||||
|
|
@ -270,4 +274,8 @@ const generateProductKey = () => {
|
|||
color: #909399;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.modbus-tip {
|
||||
color: #f56c6c;
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
<!-- Modbus 命令详情对话框 -->
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" title="Modbus 命令详情" width="700px">
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
>
|
||||
<template #title>
|
||||
此命令由系统根据您配置的 Modbus 参数自动生成,可直接用于设备通信
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<el-descriptions :column="1" border v-loading="loading">
|
||||
<!-- 完整数据帧 -->
|
||||
<el-descriptions-item label="完整数据帧">
|
||||
<div class="flex items-center justify-between">
|
||||
<el-tag type="primary" size="large" class="font-mono">
|
||||
{{ modbusConfig?.fullFrame || '-' }}
|
||||
</el-tag>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:icon="CopyDocument"
|
||||
@click="copyToClipboard(modbusConfig?.fullFrame)"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
|
||||
<!-- CRC 校验码 -->
|
||||
<el-descriptions-item label="CRC 校验码">
|
||||
<el-tag type="success" class="font-mono">
|
||||
{{ modbusConfig?.crcHex || '-' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<!-- 功能码 -->
|
||||
<el-descriptions-item label="功能码">
|
||||
<el-tag type="warning">
|
||||
{{ formatFunctionCode(modbusConfig?.functionCode) }}
|
||||
</el-tag>
|
||||
<span class="ml-2 text-gray-500">{{ getFunctionCodeDesc(modbusConfig?.functionCode) }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<!-- 寄存器地址 -->
|
||||
<el-descriptions-item label="寄存器地址">
|
||||
<el-tag>{{ modbusConfig?.registerAddress ?? '-' }}</el-tag>
|
||||
<span class="ml-2 text-gray-500">(十进制)</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<!-- 寄存器数量 -->
|
||||
<el-descriptions-item label="寄存器数量">
|
||||
<el-tag>{{ modbusConfig?.registerCount ?? '-' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<!-- 字节序 -->
|
||||
<el-descriptions-item label="字节序">
|
||||
<el-tag :type="modbusConfig?.byteOrder === 'BIG_ENDIAN' ? 'primary' : 'info'">
|
||||
{{ getByteOrderLabel(modbusConfig?.byteOrder) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<!-- 数据采集模式 -->
|
||||
<el-descriptions-item label="数据采集模式">
|
||||
<el-tag :type="modbusConfig?.collectionMode === 1 ? 'success' : 'warning'">
|
||||
{{ getCollectionModeLabel(modbusConfig?.collectionMode) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<!-- 采集频率(仅定时轮询模式) -->
|
||||
<el-descriptions-item
|
||||
v-if="modbusConfig?.collectionMode === 1"
|
||||
label="采集频率"
|
||||
>
|
||||
<el-tag type="info">
|
||||
{{ modbusConfig?.pollingInterval ?? '-' }} 秒
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CopyDocument } from '@element-plus/icons-vue'
|
||||
import { ModbusConfig, ThingModelApi } from '@/api/iot/thingmodel'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { IoTModbusCollectionModeEnum, IoTModbusByteOrderEnum } from '@/views/iot/utils/constants'
|
||||
|
||||
defineOptions({ name: 'ThingModelModbusCommandDialog' })
|
||||
|
||||
const message = useMessage()
|
||||
const { copy } = useClipboard()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const modbusConfig = ref<ModbusConfig | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
/** 打开对话框 */
|
||||
const open = async (config: ModbusConfig, productId: number) => {
|
||||
modbusConfig.value = { ...config }
|
||||
dialogVisible.value = true
|
||||
|
||||
// 如果 fullFrame 为空,实时生成命令
|
||||
if (!config.fullFrame && config.registerAddress !== undefined && config.functionCode) {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await ThingModelApi.calculateModbusCommand({
|
||||
productId: productId,
|
||||
registerAddress: config.registerAddress,
|
||||
functionCode: config.functionCode,
|
||||
registerCount: config.registerCount,
|
||||
byteOrder: config.byteOrder
|
||||
})
|
||||
|
||||
console.log('API 返回结果:', result)
|
||||
|
||||
// 更新命令数据
|
||||
if (modbusConfig.value && result) {
|
||||
modbusConfig.value.fullFrame = result.fullFrame
|
||||
modbusConfig.value.crcHex = result.crcHex
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('生成 Modbus 命令失败', error)
|
||||
message.error('生成命令失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
|
||||
/** 格式化功能码 */
|
||||
const formatFunctionCode = (code: number | undefined) => {
|
||||
if (code === undefined) return '-'
|
||||
return `0x${code.toString(16).toUpperCase().padStart(2, '0')} (${code})`
|
||||
}
|
||||
|
||||
/** 获取功能码描述 */
|
||||
const getFunctionCodeDesc = (code: number | undefined) => {
|
||||
const descriptions: Record<number, string> = {
|
||||
1: '读线圈',
|
||||
2: '读离散输入',
|
||||
3: '读保持寄存器',
|
||||
4: '读输入寄存器',
|
||||
5: '写单个线圈',
|
||||
6: '写单个寄存器',
|
||||
15: '写多个线圈',
|
||||
16: '写多个寄存器'
|
||||
}
|
||||
return code !== undefined ? descriptions[code] || '自定义功能码' : ''
|
||||
}
|
||||
|
||||
/** 获取字节序标签 */
|
||||
const getByteOrderLabel = (byteOrder: string | undefined) => {
|
||||
if (!byteOrder) return '-'
|
||||
const order = Object.values(IoTModbusByteOrderEnum).find(o => o.value === byteOrder)
|
||||
return order ? order.label : byteOrder
|
||||
}
|
||||
|
||||
/** 获取采集模式标签 */
|
||||
const getCollectionModeLabel = (mode: number | undefined) => {
|
||||
if (mode === undefined) return '-'
|
||||
const modeEnum = Object.values(IoTModbusCollectionModeEnum).find(m => m.value === mode)
|
||||
return modeEnum ? modeEnum.label : mode.toString()
|
||||
}
|
||||
|
||||
/** 复制到剪贴板 */
|
||||
const copyToClipboard = async (text: string | undefined) => {
|
||||
if (!text) {
|
||||
message.warning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await copy(text)
|
||||
message.success('已复制到剪贴板')
|
||||
} catch (error) {
|
||||
message.error('复制失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.font-mono {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 120px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.el-tag) {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
{{ mode.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tip">{{ getCollectionModeDesc() }}</div>
|
||||
|
||||
</el-form-item>
|
||||
<!-- 采集频率(仅在定时轮询模式下显示) -->
|
||||
<el-form-item
|
||||
|
|
@ -122,7 +122,6 @@
|
|||
placeholder="请输入采集频率(10-86400秒)"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="form-tip">采集频率范围:10-86400秒,默认300秒(5分钟)</div>
|
||||
</el-form-item>
|
||||
<!-- 功能码 -->
|
||||
<el-form-item label="功能码" prop="property.modbusConfig.functionCode">
|
||||
|
|
@ -134,7 +133,6 @@
|
|||
@change="handleFunctionCodeChange"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="form-tip">常用0x03-读保持寄存器,0x04-读输入寄存器,0x01-读线圈,0x02-读离散输入</div>
|
||||
</el-form-item>
|
||||
<!-- 寄存器地址 -->
|
||||
<el-form-item label="寄存器地址" prop="property.modbusConfig.registerAddress">
|
||||
|
|
@ -157,7 +155,19 @@
|
|||
@change="handleRegisterCountChange"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="form-tip">读取的寄存器或线圈的数量,默认为 1</div>
|
||||
</el-form-item>
|
||||
<!-- 字节序 -->
|
||||
<el-form-item label="字节序" prop="property.modbusConfig.byteOrder">
|
||||
<el-radio-group v-model="property.modbusConfig.byteOrder">
|
||||
<el-radio
|
||||
v-for="order in Object.values(IoTModbusByteOrderEnum)"
|
||||
:key="order.value"
|
||||
:label="order.value"
|
||||
>
|
||||
{{ order.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
</el-form-item>
|
||||
<!-- 自动生成的命令(只读显示) -->
|
||||
<el-form-item label="完整数据帧" v-if="property.modbusConfig?.fullFrame">
|
||||
|
|
@ -192,6 +202,7 @@ import {
|
|||
IoTDataSpecsDataTypeEnum,
|
||||
IoTThingModelAccessModeEnum,
|
||||
IoTModbusCollectionModeEnum,
|
||||
IoTModbusByteOrderEnum,
|
||||
IOT_PROVIDE_KEY
|
||||
} from '@/views/iot/utils/constants'
|
||||
import { ProductVO } from '@/api/iot/product/product'
|
||||
|
|
@ -288,6 +299,10 @@ watch(
|
|||
if (isModbus && property.value.modbusConfig && !property.value.modbusConfig.pollingInterval) {
|
||||
property.value.modbusConfig.pollingInterval = 300
|
||||
}
|
||||
// 向下兼容:如果没有 byteOrder,默认设置为大端模式
|
||||
if (isModbus && property.value.modbusConfig && !property.value.modbusConfig.byteOrder) {
|
||||
property.value.modbusConfig.byteOrder = IoTModbusByteOrderEnum.BIG_ENDIAN.value
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
|
@ -345,16 +360,7 @@ const handleCollectionModeChange = (mode: number) => {
|
|||
}
|
||||
}
|
||||
|
||||
/** 获取采集模式描述 */
|
||||
const getCollectionModeDesc = () => {
|
||||
const mode = property.value.modbusConfig?.collectionMode
|
||||
if (mode === IoTModbusCollectionModeEnum.POLLING.value) {
|
||||
return IoTModbusCollectionModeEnum.POLLING.desc
|
||||
} else if (mode === IoTModbusCollectionModeEnum.ACTIVE_REPORT.value) {
|
||||
return IoTModbusCollectionModeEnum.ACTIVE_REPORT.desc
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
@ -365,19 +371,10 @@ const getCollectionModeDesc = () => {
|
|||
}
|
||||
|
||||
.modbus-config-section {
|
||||
background-color: #f7f8fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.modbus-config-section .el-divider {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
:direction="IoTThingModelParamDirectionEnum.OUTPUT"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
<!-- Modbus 配置 -->
|
||||
<el-divider content-position="left">Modbus 配置(可选)</el-divider>
|
||||
<el-form-item label="完整帧">
|
||||
|
|
@ -45,12 +45,9 @@
|
|||
v-model="service.modbusConfig.functionCode"
|
||||
:min="1"
|
||||
:max="127"
|
||||
placeholder="如:5(写单个线圈)"
|
||||
placeholder="如:5"
|
||||
clearable
|
||||
/>
|
||||
<div class="form-item-tip">
|
||||
常用:05=写单线圈、06=写单寄存器、15=写多线圈、16=写多寄存器
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="寄存器地址">
|
||||
<el-input-number
|
||||
|
|
@ -64,14 +61,21 @@
|
|||
<el-form-item label="写入值">
|
||||
<el-input-number
|
||||
v-model="service.modbusConfig.writeValue"
|
||||
:min="0"
|
||||
:max="65535"
|
||||
placeholder="如:65280(0xFF00闭合)或 0(断开)"
|
||||
clearable
|
||||
placeholder="写入值"
|
||||
/>
|
||||
<div class="form-item-tip">
|
||||
功能码05:65280(0xFF00)=闭合,0(0x0000)=断开
|
||||
</div>
|
||||
</el-form-item>
|
||||
<!-- 字节序 -->
|
||||
<el-form-item label="字节序" prop="service.modbusConfig.byteOrder">
|
||||
<el-radio-group v-model="service.modbusConfig.byteOrder">
|
||||
<el-radio
|
||||
v-for="order in Object.values(IoTModbusByteOrderEnum)"
|
||||
:key="order.value"
|
||||
:label="order.value"
|
||||
>
|
||||
{{ order.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
|
|
@ -82,7 +86,8 @@ import { ThingModelService } from '@/api/iot/thingmodel'
|
|||
import { isEmpty } from '@/utils/is'
|
||||
import {
|
||||
IoTThingModelParamDirectionEnum,
|
||||
IoTThingModelServiceCallTypeEnum
|
||||
IoTThingModelServiceCallTypeEnum,
|
||||
IoTModbusByteOrderEnum
|
||||
} from '@/views/iot/utils/constants'
|
||||
|
||||
/** IoT 物模型服务 */
|
||||
|
|
@ -100,10 +105,16 @@ if (!service.value.modbusConfig) {
|
|||
writeValue: undefined,
|
||||
registerCount: undefined,
|
||||
fullFrame: '',
|
||||
crcHex: ''
|
||||
crcHex: '',
|
||||
byteOrder: IoTModbusByteOrderEnum.BIG_ENDIAN.value // 默认大端模式
|
||||
}
|
||||
} else if (!service.value.modbusConfig.byteOrder) {
|
||||
// 向下兼容:如果没有 byteOrder,默认设置为大端模式
|
||||
service.value.modbusConfig.byteOrder = IoTModbusByteOrderEnum.BIG_ENDIAN.value
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 默认选中,ASYNC 异步 */
|
||||
watch(
|
||||
() => service.value.callType,
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@
|
|||
<DataDefinition :data="row" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="操作">
|
||||
<el-table-column align="center" label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPermi="[`iot:thing-model:update`]"
|
||||
|
|
@ -81,6 +81,15 @@
|
|||
>
|
||||
删除
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="showModbusCommandButton(scope.row)"
|
||||
v-hasPermi="['iot:thing-model:query']"
|
||||
link
|
||||
type="success"
|
||||
@click="openModbusCommand(scope.row)"
|
||||
>
|
||||
查看命令
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
|
@ -97,12 +106,15 @@
|
|||
<!-- 表单弹窗:添加/修改 -->
|
||||
<ThingModelForm ref="formRef" @success="getList" />
|
||||
<ThingModelTSL ref="tslRef" />
|
||||
<!-- Modbus 命令详情对话框 -->
|
||||
<ThingModelModbusCommandDialog ref="modbusCommandDialogRef" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ThingModelApi, ThingModelData } from '@/api/iot/thingmodel'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import ThingModelForm from './ThingModelForm.vue'
|
||||
import ThingModelTSL from './ThingModelTSL.vue'
|
||||
import ThingModelModbusCommandDialog from './ThingModelModbusCommandDialog.vue'
|
||||
import { ProductVO } from '@/api/iot/product/product'
|
||||
import { getDataTypeOptionsLabel, IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
|
||||
import { DataDefinition } from './components'
|
||||
|
|
@ -169,6 +181,28 @@ const handleDelete = async (id: number) => {
|
|||
} catch {}
|
||||
}
|
||||
|
||||
/** 判断是否显示 Modbus 命令按钮 */
|
||||
const showModbusCommandButton = (row: ThingModelData) => {
|
||||
// 必须是 Modbus 产品
|
||||
const isModbusProduct = product?.value?.protocolType === 'MODBUS_RTU' || product?.value?.protocolType === 'MODBUS_TCP'
|
||||
if (!isModbusProduct) return false
|
||||
|
||||
// 必须是属性类型(type = 1)
|
||||
if (row.type !== 1) return false
|
||||
|
||||
// 必须有 modbusConfig 且配置了寄存器地址和功能码
|
||||
const config = row.property?.modbusConfig
|
||||
return !!(config && config.registerAddress !== undefined && config.functionCode)
|
||||
}
|
||||
|
||||
/** 打开 Modbus 命令详情对话框 */
|
||||
const modbusCommandDialogRef = ref()
|
||||
const openModbusCommand = (row: ThingModelData) => {
|
||||
if (row.property?.modbusConfig && product?.value?.id) {
|
||||
modbusCommandDialogRef.value?.open(row.property.modbusConfig, product.value.id)
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
|
|
|
|||
|
|
@ -196,13 +196,12 @@ export const THING_MODEL_GROUP_LABELS = {
|
|||
export const IoTModbusCollectionModeEnum = {
|
||||
POLLING: {
|
||||
label: '定时轮询',
|
||||
value: 1,
|
||||
desc: '服务器定时主动发送 Modbus 命令采集数据'
|
||||
value: 1
|
||||
},
|
||||
ACTIVE_REPORT: {
|
||||
label: '主动上报',
|
||||
value: 2,
|
||||
desc: '设备主动上报数据,服务器被动接收'
|
||||
|
||||
}
|
||||
} as const
|
||||
|
||||
|
|
@ -212,6 +211,24 @@ export const getModbusCollectionModeLabel = (value: number): string => {
|
|||
return mode?.label || String(value)
|
||||
}
|
||||
|
||||
/** IoT Modbus 字节序枚举 */
|
||||
export const IoTModbusByteOrderEnum = {
|
||||
BIG_ENDIAN: {
|
||||
label: 'ABCD',
|
||||
value: 1
|
||||
},
|
||||
LITTLE_ENDIAN: {
|
||||
label: 'CDAB',
|
||||
value: 2
|
||||
}
|
||||
} as const
|
||||
|
||||
/** 获取 Modbus 字节序标签 */
|
||||
export const getModbusByteOrderLabel = (value: number): string => {
|
||||
const order = Object.values(IoTModbusByteOrderEnum).find((order) => order.value === value)
|
||||
return order?.label || String(value)
|
||||
}
|
||||
|
||||
// IoT OTA 任务设备范围枚举
|
||||
export const IoTOtaTaskDeviceScopeEnum = {
|
||||
ALL: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue