feat: 网关管理页面 + 设备表单网关下拉联动
- 新增 src/api/gateway.js(网关 CRUD + 测试连接 + options API + 枚举映射) - 新增 src/views/config/gateway/Index.vue(网关管理页面:列表/新增/编辑/测试连接/详情抽屉+下挂设备) - 修改 src/views/config/device/Index.vue(GatewayCode 文本输入改为 GatewayId 下拉选择,列表列显示 GatewayName) - 新增 src/api/product.js、src/api/thingPoint.js(产品/物模型点 API) - 新增 src/components/thing/(PointSendForm、ThingPointTable、ThingAlarmPanel 共享组件)
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 后端路由前缀(GatewayController: [Route("api/config/gateway")])
|
||||
const base = '/config/gateway'
|
||||
|
||||
// ============ 枚举常量(与后端 IotDeviceProtocolEnum 一一对应,复用 device.js) ============
|
||||
export const PROTOCOL_TYPES = {
|
||||
1: 'Modbus TCP',
|
||||
2: 'Modbus RTU',
|
||||
3: 'S7',
|
||||
4: 'TCP',
|
||||
5: '串口'
|
||||
}
|
||||
export const protocolOptions = Object.entries(PROTOCOL_TYPES).map(([value, label]) => ({ value: Number(value), label }))
|
||||
export const protocolLabel = (v) => PROTOCOL_TYPES[v] || '-'
|
||||
|
||||
// 在线状态
|
||||
export const ONLINE_STATUS = {
|
||||
0: { label: '离线', tag: 'info' },
|
||||
1: { label: '在线', tag: 'success' },
|
||||
3: { label: '异常', tag: 'danger' }
|
||||
}
|
||||
export const onlineStatusLabel = (s) => ONLINE_STATUS[s]?.label || '离线'
|
||||
export const onlineStatusTag = (s) => ONLINE_STATUS[s]?.tag || 'info'
|
||||
|
||||
// 校验位
|
||||
export const PARITY_OPTIONS = [
|
||||
{ value: 0, label: 'None' },
|
||||
{ value: 1, label: 'Odd' },
|
||||
{ value: 2, label: 'Even' }
|
||||
]
|
||||
|
||||
// ============ 网关接口 ============
|
||||
|
||||
// 网关列表(分页;X-Total-Count)
|
||||
export const getGatewayList = (params) => request.get(`${base}/list`, { params })
|
||||
|
||||
// 网关详情
|
||||
export const getGatewayDetail = (id) => request.get(`${base}/${id}`)
|
||||
|
||||
// 新增网关
|
||||
export const addGateway = (data) => request.post(base, data)
|
||||
|
||||
// 修改网关
|
||||
export const updateGateway = (data) => request.put(base, data)
|
||||
|
||||
// 删除网关
|
||||
export const deleteGateway = (id) => request.delete(`${base}/${id}`)
|
||||
|
||||
// 测试连接
|
||||
export const testGatewayConnection = (id) => request.post(`${base}/${id}/test-connection`)
|
||||
|
||||
// 下拉选项(设备表单用)
|
||||
export const getGatewayOptions = () => request.get(`${base}/options`)
|
||||
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 后端路由前缀(ProductController: [Route("api/config/product")])
|
||||
const base = '/config/product'
|
||||
|
||||
// ============ 枚举常量(与后端 Model.Entity.Config 枚举一一对应) ============
|
||||
|
||||
// 通讯协议(IotDeviceProtocolEnum)
|
||||
export const PROTOCOL_TYPES = {
|
||||
1: 'Modbus TCP',
|
||||
2: 'Modbus RTU',
|
||||
3: 'S7',
|
||||
4: 'TCP',
|
||||
5: '串口'
|
||||
}
|
||||
export const protocolOptions = Object.entries(PROTOCOL_TYPES).map(([value, label]) => ({ value: Number(value), label }))
|
||||
export const protocolLabel = (v) => PROTOCOL_TYPES[v] || '-'
|
||||
|
||||
// ============ 产品分类接口 ============
|
||||
|
||||
export const getProductCategories = () => request.get(`${base}/category/list`)
|
||||
export const addProductCategory = (data) => request.post(`${base}/category/add`, data)
|
||||
export const updateProductCategory = (data) => request.put(`${base}/category/update`, data)
|
||||
export const deleteProductCategory = (id) => request.delete(`${base}/category/${id}`)
|
||||
|
||||
// ============ 产品接口 ============
|
||||
|
||||
// 产品下拉选项(设备选择所属产品用)
|
||||
export const getProductOptions = () => request.get(`${base}/options`)
|
||||
|
||||
// 产品列表(服务端分页;结果数组上附带 total,来自响应头 X-Total-Count)
|
||||
export const getProductList = (params) => request.get(`${base}/list`, { params })
|
||||
|
||||
// 产品详情
|
||||
export const getProductDetail = (id) => request.get(`${base}/${id}`)
|
||||
|
||||
// 新增产品(后端校验型号唯一)
|
||||
export const addProduct = (data) => request.post(base, data)
|
||||
|
||||
// 修改产品
|
||||
export const updateProduct = (data) => request.put(base, data)
|
||||
|
||||
// 删除产品(软删除)
|
||||
export const deleteProduct = (id) => request.delete(`${base}/${id}`)
|
||||
@@ -0,0 +1,57 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 后端路由前缀(ThingPointController: [Route("api/config/thing-point")])
|
||||
const base = '/config/thing-point'
|
||||
|
||||
// ============ 枚举常量(与后端 Model.Entity.Config.ThingEnums 对应) ============
|
||||
|
||||
// 归属类型(ThingOwnerTypeEnum)
|
||||
export const OWNER_TYPES = { 1: '产品', 2: '设备' }
|
||||
|
||||
// 读写权限(ThingRwEnum)
|
||||
export const RW_TYPES = {
|
||||
1: { label: '只读' },
|
||||
2: { label: '只写' },
|
||||
3: { label: '读写' }
|
||||
}
|
||||
export const rwOptions = Object.entries(RW_TYPES).map(([value, v]) => ({ value: Number(value), label: v.label }))
|
||||
export const rwLabel = (v) => RW_TYPES[v]?.label || '-'
|
||||
|
||||
// 寄存器类型(ThingRegisterTypeEnum)
|
||||
export const REGISTER_TYPES = {
|
||||
1: { label: '线圈', canWrite: true },
|
||||
2: { label: '保持寄存器', canWrite: true },
|
||||
3: { label: '输入寄存器', canWrite: false }
|
||||
}
|
||||
export const registerOptions = Object.entries(REGISTER_TYPES).map(([value, v]) => ({ value: Number(value), label: v.label }))
|
||||
export const registerLabel = (v) => REGISTER_TYPES[v]?.label || '-'
|
||||
|
||||
// 数据类型(ThingDataTypeEnum)
|
||||
export const DATA_TYPES = {
|
||||
1: { label: 'Bool' },
|
||||
2: { label: 'Int16' },
|
||||
3: { label: 'UInt16' },
|
||||
4: { label: 'Int32' },
|
||||
5: { label: 'Float' }
|
||||
}
|
||||
export const dataOptions = Object.entries(DATA_TYPES).map(([value, v]) => ({ value: Number(value), label: v.label }))
|
||||
export const dataLabel = (v) => DATA_TYPES[v]?.label || '-'
|
||||
|
||||
// ============ 物模型点接口 ============
|
||||
|
||||
// 点列表(ownerType: 1=产品 2=设备)
|
||||
export const getThingPoints = (ownerType, ownerId, keyword) => request.get(`${base}/list`, {
|
||||
params: { ownerType, ownerId, keyword: keyword || undefined }
|
||||
})
|
||||
|
||||
// 点详情
|
||||
export const getThingPoint = (id) => request.get(`${base}/${id}`)
|
||||
|
||||
// 新增点
|
||||
export const addThingPoint = (data) => request.post(base, data)
|
||||
|
||||
// 修改点
|
||||
export const updateThingPoint = (data) => request.put(base, data)
|
||||
|
||||
// 删除点
|
||||
export const deleteThingPoint = (id) => request.delete(`${base}/${id}`)
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="point-send-form">
|
||||
<div class="ps-toolbar">
|
||||
<span class="ps-count">
|
||||
共 {{ points.length }} 个点,可下发 {{ writablePoints.length }} 个
|
||||
<span v-if="points.length" class="ps-tip">(只读点不支持下发;Int32/Float 从地址起连写 2 个寄存器)</span>
|
||||
</span>
|
||||
<el-button link type="primary" size="small" :loading="loading" @click="fetchPoints">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="points" v-loading="loading" size="small" border stripe>
|
||||
<el-table-column prop="Code" label="编码" width="120" />
|
||||
<el-table-column prop="Name" label="名称" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="寄存器" width="90">
|
||||
<template #default="{ row }">{{ registerLabel(row.RegisterType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地址" width="70" align="center">
|
||||
<template #default="{ row }">0x{{ dec2hex(row.Address) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="读写" width="60" align="center">
|
||||
<template #default="{ row }">{{ rwLabel(row.Rw) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="84" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.Rw === 1" size="small" type="info">只读</el-tag>
|
||||
<el-tag v-else size="small" type="success">可下发</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下发值" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-if="row.DataType === 1"
|
||||
:model-value="row._sendValue === 1"
|
||||
active-text="开"
|
||||
@change="row._sendValue = $event ? 1 : 0"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else
|
||||
v-model="row._sendValue"
|
||||
:precision="row.DataType === 5 ? 1 : 0"
|
||||
:step="1"
|
||||
:controls="false"
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" width="60" align="center">
|
||||
<template #default="{ row }">{{ row.Unit || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="70" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link type="primary"
|
||||
:disabled="row.Rw === 1"
|
||||
@click="onSend(row)"
|
||||
>下发</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 空状态区分:无点 / 点均不可下发 -->
|
||||
<el-empty
|
||||
v-if="!loading && points.length === 0"
|
||||
description="该设备暂无物模型点,请先在「物模型点」标签页为该设备添加数据点"
|
||||
:image-size="60"
|
||||
/>
|
||||
<el-alert
|
||||
v-else-if="!loading && writablePoints.length === 0"
|
||||
title="已添加的点均为只读,暂无可下发点(可将点读写属性改为「只写/读写」)"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="ps-feedback"
|
||||
/>
|
||||
|
||||
<!-- 下发反馈 -->
|
||||
<el-alert v-if="feedback.text" :title="feedback.text" :type="feedback.type" :closable="false" class="ps-feedback" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getThingPoints, registerLabel, rwLabel } from '@/api/thingPoint'
|
||||
import { sendDeviceCommand } from '@/api/device'
|
||||
|
||||
const props = defineProps({
|
||||
deviceId: { type: String, required: true }
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const points = ref([])
|
||||
const feedback = ref({ text: '', type: 'success' })
|
||||
|
||||
// 只读(1)的点不能下发;只写/读写都可以(Int32/Float 走 FC16 连写 2 个寄存器,已支持)
|
||||
const writablePoints = computed(() => points.value.filter(p => p.Rw !== 1))
|
||||
|
||||
function dec2hex(n) {
|
||||
return (Number(n) || 0).toString(16).toUpperCase().padStart(4, '0')
|
||||
}
|
||||
|
||||
async function fetchPoints() {
|
||||
loading.value = true
|
||||
try {
|
||||
const pts = (await getThingPoints(2, props.deviceId)) || []
|
||||
// 显示设备全部点(含只读),便于与「物模型点」标签页对照
|
||||
points.value = pts.map(p => ({ ...p, _sendValue: p.DataType === 1 ? 1 : 0 }))
|
||||
} catch (e) {
|
||||
points.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onSend(row) {
|
||||
try {
|
||||
await sendDeviceCommand(props.deviceId, {
|
||||
Id: props.deviceId,
|
||||
PointId: row.Id,
|
||||
Value: row._sendValue ?? 0,
|
||||
IsSimulated: false
|
||||
})
|
||||
// res 为后端返回的 Data(无数据时返回 undefined),消息提示走统一处理
|
||||
feedback.value = { text: `已下发至「${row.Name || row.Code}」`, type: 'success' }
|
||||
ElMessage.success(`指令已下发:${row.Name || row.Code}`)
|
||||
} catch (e) {
|
||||
feedback.value = { text: e?.message || '下发失败', type: 'error' }
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.deviceId, (val) => { if (val) fetchPoints() })
|
||||
onMounted(() => { if (props.deviceId) fetchPoints() })
|
||||
|
||||
defineExpose({ refresh: fetchPoints })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ps-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ps-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
.ps-tip {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
.ps-feedback {
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="thing-alarm-panel">
|
||||
<div class="tp-toolbar">
|
||||
<el-button type="primary" size="small" icon="Plus" @click="openAdd">新增告警规则</el-button>
|
||||
<span v-if="rules.length" class="tp-count">共 {{ rules.length }} 条规则</span>
|
||||
</div>
|
||||
|
||||
<el-table :data="rules" v-loading="loading" size="small" border stripe>
|
||||
<el-table-column prop="RuleName" label="规则名称" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="绑定点" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.PointCode" size="small" type="info">{{ row.PointCode }}</el-tag>
|
||||
<span v-else>整{{ ownerTypeLabel }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="触发条件" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ metricLabel(row.Metric) }} {{ row.Operator }} {{ row.Threshold }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="级别" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="levelTagType(row.AlarmLevel)" size="small">{{ row.AlarmLevel }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch :model-value="row.Enabled" size="small" @change="onToggle(row, $event)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="130" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="form.Id ? '编辑告警规则' : '新增告警规则'" width="520px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="rulesForm" label-width="100px">
|
||||
<el-form-item label="规则名称" prop="RuleName">
|
||||
<el-input v-model="form.RuleName" placeholder="如:温度超限报警" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="监测指标" prop="Metric">
|
||||
<el-select v-model="form.Metric" placeholder="请选择该归属下的点" style="width: 100%">
|
||||
<el-option v-for="p in pointOptions" :key="p.value" :label="p.label" :value="p.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="触发条件" required>
|
||||
<div style="display: flex; gap: 8px; width: 100%">
|
||||
<el-select v-model="form.Operator" placeholder="运算符" style="width: 45%">
|
||||
<el-option v-for="op in operators" :key="op" :label="op" :value="op" />
|
||||
</el-select>
|
||||
<el-input-number v-model="form.Threshold" :precision="1" :step="1" style="width: 55%" placeholder="阈值" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="告警级别" prop="AlarmLevel">
|
||||
<el-select v-model="form.AlarmLevel" style="width: 100%">
|
||||
<el-option v-for="l in levels" :key="l" :label="l" :value="l" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="form.Enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.Remark" type="textarea" :rows="2" maxlength="500" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="onSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getOwnerAlertRules, addAlertRule, updateAlertRule, deleteAlertRule, setAlertRuleEnabled } from '@/api/alertRule'
|
||||
import { getThingPoints } from '@/api/thingPoint'
|
||||
|
||||
const props = defineProps({
|
||||
ownerType: { type: Number, required: true }, // 1=产品 2=设备
|
||||
ownerId: { type: String, required: true }
|
||||
})
|
||||
|
||||
const ownerTypeLabel = computed(() => (props.ownerType === 1 ? '产品' : '设备'))
|
||||
|
||||
const operators = ['>', '>=', '<', '<=', '=', '≠']
|
||||
const levels = ['信息', '警告', '紧急']
|
||||
const levelTagType = (l) => ({ '信息': 'info', '警告': 'warning', '紧急': 'danger' }[l] || 'info')
|
||||
|
||||
const loading = ref(false)
|
||||
const rules = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const formRef = ref(null)
|
||||
const pointOptions = ref([])
|
||||
|
||||
const defaultForm = () => ({
|
||||
Id: '', RuleName: '', DeviceType: '', Metric: '', Operator: '>',
|
||||
Threshold: null, AlarmLevel: '警告', Enabled: true, Remark: '',
|
||||
OwnerType: props.ownerType, OwnerId: props.ownerId, PointCode: ''
|
||||
})
|
||||
const form = reactive(defaultForm())
|
||||
|
||||
const rulesForm = {
|
||||
RuleName: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
|
||||
Metric: [{ required: true, message: '请选择监测指标', trigger: 'change' }],
|
||||
AlarmLevel: [{ required: true, message: '请选择告警级别', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const metricLabel = (m) => pointOptions.value.find(p => p.value === m)?.label || m || '-'
|
||||
|
||||
async function fetchRules() {
|
||||
loading.value = true
|
||||
try {
|
||||
rules.value = (await getOwnerAlertRules(props.ownerType, props.ownerId)) || []
|
||||
} catch (e) {
|
||||
rules.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPoints() {
|
||||
try {
|
||||
const pts = (await getThingPoints(props.ownerType, props.ownerId)) || []
|
||||
pointOptions.value = pts.map(p => ({ value: p.Code, label: `${p.Name || p.Code} (${p.Code})` }))
|
||||
} catch (e) {
|
||||
pointOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
Object.assign(form, defaultForm())
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
Object.assign(form, defaultForm(), row)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
await formRef.value.validate()
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
...form,
|
||||
OwnerType: props.ownerType,
|
||||
OwnerId: Number(props.ownerId) || 0,
|
||||
// 从点选 Metric 时同步 PointCode(点编码即绑定编码)
|
||||
PointCode: form.Metric || null
|
||||
}
|
||||
if (payload.Id) {
|
||||
await updateAlertRule(payload)
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
delete payload.Id
|
||||
await addAlertRule(payload)
|
||||
ElMessage.success('新增成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchRules()
|
||||
} catch (e) { /* 拦截器已提示 */ } finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggle(row, enabled) {
|
||||
try {
|
||||
await setAlertRuleEnabled(row.Id, enabled)
|
||||
row.Enabled = enabled
|
||||
ElMessage.success(enabled ? '已启用' : '已停用')
|
||||
} catch (e) { /* 拦截器已提示 */ }
|
||||
}
|
||||
|
||||
async function onDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除规则「${row.RuleName}」吗?`, '提示', { type: 'warning' })
|
||||
try {
|
||||
await deleteAlertRule(row.Id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchRules()
|
||||
} catch (e) { /* 拦截器已提示 */ }
|
||||
}
|
||||
|
||||
watch(() => props.ownerId, (val) => { if (val) { fetchPoints(); fetchRules() } })
|
||||
onMounted(() => { if (props.ownerId) { fetchPoints(); fetchRules() } })
|
||||
|
||||
defineExpose({ refresh: fetchRules })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tp-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.tp-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<div class="thing-point-table">
|
||||
<div class="tp-toolbar">
|
||||
<el-button type="primary" size="small" icon="Plus" @click="openAdd">新增点</el-button>
|
||||
<span v-if="points.length" class="tp-count">共 {{ points.length }} 个点</span>
|
||||
</div>
|
||||
|
||||
<el-table :data="points" v-loading="loading" size="small" border stripe>
|
||||
<el-table-column prop="Code" label="编码" width="120" />
|
||||
<el-table-column prop="Name" label="名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="寄存器" width="90">
|
||||
<template #default="{ row }">{{ registerLabel(row.RegisterType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="读写" width="70" align="center">
|
||||
<template #default="{ row }">{{ rwLabel(row.Rw) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template #default="{ row }">{{ dataLabel(row.DataType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地址" width="80" align="center">
|
||||
<template #default="{ row }">0x{{ dec2hex(row.Address) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="倍率" width="70" align="center">
|
||||
<template #default="{ row }">{{ row.Scale }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="Unit" label="单位" width="60" align="center">
|
||||
<template #default="{ row }">{{ row.Unit || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.Enabled ? 'success' : 'info'" size="small">{{ row.Enabled ? '启用' : '停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="onDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="form.Id ? '编辑点' : '新增点'" width="560px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="点编码" prop="Code">
|
||||
<el-input v-model="form.Code" placeholder="如 TempPV / PowerOn" maxlength="64" />
|
||||
</el-form-item>
|
||||
<el-form-item label="点名称" prop="Name">
|
||||
<el-input v-model="form.Name" placeholder="如 温度PV" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="寄存器类型" prop="RegisterType">
|
||||
<el-select v-model="form.RegisterType" style="width: 100%">
|
||||
<el-option v-for="o in registerOptions" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="读写权限" prop="Rw">
|
||||
<el-select v-model="form.Rw" style="width: 100%">
|
||||
<el-option v-for="o in rwOptions" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="数据类型" prop="DataType">
|
||||
<el-select v-model="form.DataType" style="width: 100%">
|
||||
<el-option v-for="o in dataOptions" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="寄存器地址" prop="Address">
|
||||
<el-input-number v-model="form.Address" :min="0" :max="65535" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="倍率 Scale">
|
||||
<el-input-number v-model="form.Scale" :min="0.0001" :precision="4" :step="1" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="偏移 Offset">
|
||||
<el-input-number v-model="form.Offset" :precision="4" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="单位">
|
||||
<el-input v-model="form.Unit" placeholder="如 ℃ / %" maxlength="16" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序号">
|
||||
<el-input-number v-model="form.Sort" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="是否启用">
|
||||
<el-switch v-model="form.Enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.Remark" type="textarea" :rows="2" maxlength="500" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="onSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getThingPoints, addThingPoint, updateThingPoint, deleteThingPoint,
|
||||
rwOptions, rwLabel, registerOptions, registerLabel, dataOptions, dataLabel
|
||||
} from '@/api/thingPoint'
|
||||
|
||||
const props = defineProps({
|
||||
ownerType: { type: Number, required: true }, // 1=产品 2=设备
|
||||
ownerId: { type: String, required: true }
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const points = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
const defaultForm = () => ({
|
||||
Id: '', Code: '', Name: '', OwnerType: props.ownerType, OwnerId: props.ownerId,
|
||||
RegisterType: 2, Rw: 1, DataType: 3, Address: 0,
|
||||
Scale: 1, Offset: 0, Unit: '', Enabled: true, Sort: 0, Remark: ''
|
||||
})
|
||||
const form = reactive(defaultForm())
|
||||
|
||||
const rules = {
|
||||
Code: [{ required: true, message: '请输入点编码', trigger: 'blur' }],
|
||||
Name: [{ required: true, message: '请输入点名称', trigger: 'blur' }],
|
||||
RegisterType: [{ required: true, message: '请选择寄存器类型', trigger: 'change' }],
|
||||
Rw: [{ required: true, message: '请选择读写权限', trigger: 'change' }],
|
||||
DataType: [{ required: true, message: '请选择数据类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function dec2hex(n) {
|
||||
return (Number(n) || 0).toString(16).toUpperCase().padStart(4, '0')
|
||||
}
|
||||
|
||||
async function fetchPoints() {
|
||||
loading.value = true
|
||||
try {
|
||||
points.value = (await getThingPoints(props.ownerType, props.ownerId)) || []
|
||||
} catch (e) {
|
||||
points.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
Object.assign(form, defaultForm(), { OwnerType: props.ownerType, OwnerId: props.ownerId })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
Object.assign(form, defaultForm(), {
|
||||
Id: row.Id, Code: row.Code, Name: row.Name,
|
||||
RegisterType: row.RegisterType, Rw: row.Rw, DataType: row.DataType,
|
||||
Address: row.Address, Scale: row.Scale, Offset: row.Offset,
|
||||
Unit: row.Unit || '', Enabled: row.Enabled, Sort: row.Sort, Remark: row.Remark || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
await formRef.value.validate()
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = { ...form, OwnerType: props.ownerType, OwnerId: props.ownerId }
|
||||
if (payload.Id) {
|
||||
await updateThingPoint(payload)
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
delete payload.Id
|
||||
await addThingPoint(payload)
|
||||
ElMessage.success('新增成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchPoints()
|
||||
} catch (e) { /* 拦截器已提示 */ } finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除点「${row.Code}」吗?`, '提示', { type: 'warning' })
|
||||
try {
|
||||
await deleteThingPoint(row.Id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchPoints()
|
||||
} catch (e) { /* 拦截器已提示 */ }
|
||||
}
|
||||
|
||||
watch(() => props.ownerId, (val) => { if (val) fetchPoints() })
|
||||
onMounted(() => { if (props.ownerId) fetchPoints() })
|
||||
|
||||
defineExpose({ refresh: fetchPoints })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tp-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.tp-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
@@ -48,7 +48,11 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="GatewayCode" label="所属网关" width="130" show-overflow-tooltip />
|
||||
<el-table-column label="所属网关" width="130" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.GatewayName || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="SlaveId" label="从站地址" width="90" align="center" />
|
||||
<el-table-column label="协议" width="110">
|
||||
<template #default="{ row }">{{ protocolLabel(row.ProtocolType) }}</template>
|
||||
@@ -136,8 +140,10 @@
|
||||
<el-option v-for="p in productOptions" :key="p.Id" :label="`${p.Model}${p.Name ? ' - ' + p.Name : ''}`" :value="p.Id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属网关" prop="GatewayCode">
|
||||
<el-input v-model="form.GatewayCode" placeholder="如 GW-ENV-01(IP:端口或串口标识)" maxlength="64" />
|
||||
<el-form-item label="所属网关">
|
||||
<el-select v-model="form.GatewayId" placeholder="未关联" clearable style="width: 100%">
|
||||
<el-option v-for="g in gatewayOptions" :key="g.Id" :label="`${g.Code} - ${g.Name}`" :value="g.Id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="从站地址" prop="SlaveId">
|
||||
<el-input-number v-model="form.SlaveId" :min="1" :max="247" style="width: 100%" />
|
||||
@@ -172,7 +178,7 @@
|
||||
<el-descriptions-item label="设备名称">{{ detailDevice.Name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="设备类型">{{ detailDevice.DeviceType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属产品">{{ detailDevice.ProductName || '未分类' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属网关">{{ detailDevice.GatewayCode }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属网关">{{ detailDevice.GatewayName || '未关联' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="从站地址">{{ detailDevice.SlaveId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="通讯协议">{{ protocolLabel(detailDevice.ProtocolType) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="在线状态">
|
||||
@@ -245,6 +251,7 @@ import {
|
||||
protocolOptions, protocolLabel, onlineStatusLabel, onlineStatusTag
|
||||
} from '@/api/device'
|
||||
import { getProductOptions } from '@/api/product'
|
||||
import { getGatewayOptions } from '@/api/gateway'
|
||||
import { getCurve } from '@/api/dashboard'
|
||||
import ThingPointTable from '@/components/thing/ThingPointTable.vue'
|
||||
import ThingAlarmPanel from '@/components/thing/ThingAlarmPanel.vue'
|
||||
@@ -259,6 +266,7 @@ const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const searchProductId = ref('')
|
||||
const productOptions = ref([])
|
||||
const gatewayOptions = ref([])
|
||||
|
||||
async function fetchProductOptions() {
|
||||
try {
|
||||
@@ -268,6 +276,14 @@ async function fetchProductOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchGatewayOptions() {
|
||||
try {
|
||||
gatewayOptions.value = (await getGatewayOptions()) || []
|
||||
} catch (e) {
|
||||
gatewayOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -301,7 +317,7 @@ const deviceTypes = [
|
||||
]
|
||||
|
||||
const defaultForm = () => ({
|
||||
Id: '', Code: '', Name: '', DeviceType: '', ProductId: '', GatewayCode: '',
|
||||
Id: '', Code: '', Name: '', DeviceType: '', ProductId: '', GatewayId: '',
|
||||
SlaveId: 1, ProtocolType: 1, Manufacturer: '', IsEnabled: true, Remark: ''
|
||||
})
|
||||
const form = reactive(defaultForm())
|
||||
@@ -309,8 +325,7 @@ const form = reactive(defaultForm())
|
||||
const formRules = {
|
||||
Code: [{ required: true, message: '请输入设备编号', trigger: 'blur' }],
|
||||
Name: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
|
||||
DeviceType: [{ required: true, message: '请选择设备类型', trigger: 'change' }],
|
||||
GatewayCode: [{ required: true, message: '请输入所属网关', trigger: 'blur' }]
|
||||
DeviceType: [{ required: true, message: '请选择设备类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
@@ -325,7 +340,7 @@ function openEdit(row) {
|
||||
Name: row.Name,
|
||||
DeviceType: row.DeviceType,
|
||||
ProductId: row.ProductId && row.ProductId !== '0' ? row.ProductId : '',
|
||||
GatewayCode: row.GatewayCode,
|
||||
GatewayId: row.GatewayId && row.GatewayId !== '0' ? row.GatewayId : '',
|
||||
SlaveId: row.SlaveId,
|
||||
ProtocolType: row.ProtocolType,
|
||||
Manufacturer: row.Manufacturer || '',
|
||||
@@ -394,6 +409,7 @@ function onMoreCommand(cmd, row) {
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
fetchProductOptions()
|
||||
fetchGatewayOptions()
|
||||
window.addEventListener('click', closeCtxMenu)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -2,13 +2,314 @@
|
||||
<div class="page-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>网关管理</span>
|
||||
<div class="card-header">
|
||||
<span>网关管理({{ total }} 个)</span>
|
||||
<el-button type="primary" icon="Plus" @click="openAdd">添加网关</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty description="网关管理 - 功能开发中" />
|
||||
|
||||
<div class="search-bar">
|
||||
<el-select v-model="searchProtocol" placeholder="协议类型(全部)" clearable style="width: 160px" @change="onSearch">
|
||||
<el-option v-for="p in protocolOptions" :key="p.value" :label="p.label" :value="p.value" />
|
||||
</el-select>
|
||||
<el-input v-model="keyword" placeholder="网关编码 / 名称" clearable style="width: 260px" @keyup.enter="onSearch" @clear="onSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||
<el-button @click="onReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="list" v-loading="loading" stripe border row-key="Id">
|
||||
<el-table-column prop="Code" label="网关编码" width="130" />
|
||||
<el-table-column prop="Name" label="网关名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="协议" width="110">
|
||||
<template #default="{ row }">{{ protocolLabel(row.ProtocolType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="连接地址" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ connAddress(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="在线状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="onlineStatusTag(row.OnlineStatus)" size="small">{{ onlineStatusLabel(row.OnlineStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最后连接" width="160">
|
||||
<template #default="{ row }">{{ fmtTime(row.LastConnectedTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="70" align="center">
|
||||
<template #default="{ row }"><el-switch :model-value="row.IsEnabled" disabled size="small" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
|
||||
<el-button link type="primary" @click="onTest(row)" :loading="row._testing">测试</el-button>
|
||||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="onDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :total="total"
|
||||
:page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="fetchList" @size-change="onSizeChange" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加/编辑网关 -->
|
||||
<el-dialog v-model="dialogVisible" :title="form.Id ? '修改网关' : '添加网关'" width="600px" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="网关编码" prop="Code">
|
||||
<el-input v-model="form.Code" placeholder="唯一编码,如 GW-ENV-01" maxlength="64" />
|
||||
</el-form-item>
|
||||
<el-form-item label="网关名称" prop="Name">
|
||||
<el-input v-model="form.Name" placeholder="请输入网关名称" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="协议类型" prop="ProtocolType">
|
||||
<el-select v-model="form.ProtocolType" style="width: 100%" @change="onProtocolChange">
|
||||
<el-option v-for="p in protocolOptions" :key="p.value" :label="p.label" :value="p.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- TCP 参数组 -->
|
||||
<template v-if="isTcpProtocol">
|
||||
<el-form-item label="IP 地址" prop="Host">
|
||||
<el-input v-model="form.Host" placeholder="如 192.168.1.10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="端口号" prop="Port">
|
||||
<el-input-number v-model="form.Port" :min="1" :max="65535" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 串口参数组 -->
|
||||
<template v-if="isSerialProtocol">
|
||||
<el-form-item label="串口号" prop="ComPort">
|
||||
<el-input v-model="form.ComPort" placeholder="如 COM3" maxlength="20" />
|
||||
</el-form-item>
|
||||
<el-form-item label="波特率" prop="BaudRate">
|
||||
<el-select v-model="form.BaudRate" style="width: 100%">
|
||||
<el-option v-for="b in [9600,19200,38400,57600,115200]" :key="b" :label="b" :value="b" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据位">
|
||||
<el-select v-model="form.DataBits" style="width: 100%">
|
||||
<el-option v-for="d in [7,8]" :key="d" :label="d" :value="d" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="停止位">
|
||||
<el-select v-model="form.StopBits" style="width: 100%">
|
||||
<el-option :value="1" /><el-option :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="校验位">
|
||||
<el-select v-model="form.Parity" style="width: 100%">
|
||||
<el-option v-for="p in PARITY_OPTIONS" :key="p.value" :label="p.label" :value="p.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- S7 额外参数 -->
|
||||
<template v-if="form.ProtocolType === 3">
|
||||
<el-form-item label="CPU 型号">
|
||||
<el-select v-model="form.CpuType" style="width: 100%">
|
||||
<el-option v-for="c in ['S71200','S71500','S7300','S7400']" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Rack">
|
||||
<el-input-number v-model="form.Rack" :min="0" :max="7" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Slot">
|
||||
<el-input-number v-model="form.Slot" :min="0" :max="31" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="form.IsEnabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.Remark" type="textarea" :rows="2" maxlength="500" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="onSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 网关详情抽屉 -->
|
||||
<el-drawer v-model="detailVisible" :title="`网关详情 - ${detailRow?.Name || ''}`" size="720px">
|
||||
<el-tabs v-if="detailRow" v-model="detailTab">
|
||||
<el-tab-pane label="基本信息" name="info">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="网关编码">{{ detailRow.Code }}</el-descriptions-item>
|
||||
<el-descriptions-item label="网关名称">{{ detailRow.Name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="协议类型">{{ protocolLabel(detailRow.ProtocolType) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="连接地址">{{ connAddress(detailRow) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="在线状态">
|
||||
<el-tag :type="onlineStatusTag(detailRow.OnlineStatus)" size="small">{{ onlineStatusLabel(detailRow.OnlineStatus) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最后连接">{{ fmtTime(detailRow.LastConnectedTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最近错误" :span="2">{{ detailRow.LastError || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="启用">{{ detailRow.IsEnabled ? '是' : '否' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detailRow.Remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="下挂设备" name="devices">
|
||||
<el-table :data="gatewayDevices" v-loading="devicesLoading" size="small" border stripe max-height="400">
|
||||
<el-table-column prop="Code" label="设备编号" width="130" />
|
||||
<el-table-column prop="Name" label="设备名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="SlaveId" label="从站号" width="80" align="center" />
|
||||
<el-table-column label="在线" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.OnlineStatus === 1 ? 'success' : 'info'" size="small">{{ row.OnlineStatus === 1 ? '在线' : '离线' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!devicesLoading && gatewayDevices.length === 0" description="该网关下暂无设备" :image-size="60" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// TODO: 实现网关管理功能
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getGatewayList, addGateway, updateGateway, deleteGateway, testGatewayConnection,
|
||||
protocolOptions, protocolLabel, onlineStatusLabel, onlineStatusTag, PARITY_OPTIONS
|
||||
} from '@/api/gateway'
|
||||
import { getDeviceList } from '@/api/device'
|
||||
|
||||
// ---------- 列表 ----------
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const keyword = ref('')
|
||||
const searchProtocol = ref(null)
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
list.value = (await getGatewayList({
|
||||
pageIndex: page.value, pageSize: pageSize.value,
|
||||
keyword: keyword.value || undefined,
|
||||
protocolType: searchProtocol.value || undefined
|
||||
})) || []
|
||||
total.value = list.value.total || 0
|
||||
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||
}
|
||||
|
||||
function onSearch() { page.value = 1; fetchList() }
|
||||
function onReset() { keyword.value = ''; searchProtocol.value = null; page.value = 1; fetchList() }
|
||||
function onSizeChange() { page.value = 1; fetchList() }
|
||||
|
||||
function connAddress(row) {
|
||||
if ([1, 3, 4].includes(row.ProtocolType)) return row.Host && row.Port ? `${row.Host}:${row.Port}` : '-'
|
||||
if ([2, 5].includes(row.ProtocolType)) return row.ComPort ? `${row.ComPort} @${row.BaudRate || 9600}` : '-'
|
||||
return '-'
|
||||
}
|
||||
|
||||
// ---------- 测试连接 ----------
|
||||
async function onTest(row) {
|
||||
row._testing = true
|
||||
try {
|
||||
const res = await testGatewayConnection(row.Id)
|
||||
ElMessage({ type: res.Success ? 'success' : 'error', message: res.Message || (res.Success ? '连接成功' : '连接失败') })
|
||||
fetchList()
|
||||
} catch (e) { /* 拦截器已提示 */ } finally { row._testing = false }
|
||||
}
|
||||
|
||||
// ---------- 新增/修改 ----------
|
||||
const dialogVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
const defaultForm = () => ({
|
||||
Id: '', Code: '', Name: '', ProtocolType: 1,
|
||||
Host: '', Port: 502,
|
||||
ComPort: 'COM1', BaudRate: 9600, DataBits: 8, StopBits: 1, Parity: 0,
|
||||
CpuType: 'S71200', Rack: 0, Slot: 1,
|
||||
IsEnabled: true, Remark: ''
|
||||
})
|
||||
const form = reactive(defaultForm())
|
||||
|
||||
const isTcpProtocol = computed(() => [1, 3, 4].includes(form.ProtocolType))
|
||||
const isSerialProtocol = computed(() => [2, 5].includes(form.ProtocolType))
|
||||
|
||||
function onProtocolChange() {
|
||||
// 切协议时重置连接参数
|
||||
form.Host = ''; form.Port = 502
|
||||
form.ComPort = 'COM1'; form.BaudRate = 9600
|
||||
form.CpuType = 'S71200'; form.Rack = 0; form.Slot = 1
|
||||
}
|
||||
|
||||
const formRules = {
|
||||
Code: [{ required: true, message: '请输入网关编码', trigger: 'blur' }],
|
||||
Name: [{ required: true, message: '请输入网关名称', trigger: 'blur' }],
|
||||
ProtocolType: [{ required: true, message: '请选择协议类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function openAdd() { Object.assign(form, defaultForm()); dialogVisible.value = true }
|
||||
function openEdit(row) {
|
||||
Object.assign(form, defaultForm(), {
|
||||
Id: row.Id, Code: row.Code, Name: row.Name, ProtocolType: row.ProtocolType,
|
||||
Host: row.Host || '', Port: row.Port || 502,
|
||||
ComPort: row.ComPort || 'COM1', BaudRate: row.BaudRate || 9600,
|
||||
DataBits: row.DataBits || 8, StopBits: row.StopBits || 1, Parity: row.Parity || 0,
|
||||
CpuType: row.CpuType || 'S71200', Rack: row.Rack ?? 0, Slot: row.Slot ?? 1,
|
||||
IsEnabled: row.IsEnabled, Remark: row.Remark || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
await formRef.value.validate()
|
||||
saving.value = true
|
||||
try {
|
||||
if (form.Id) { await updateGateway({ ...form }); ElMessage.success('修改成功') }
|
||||
else { const payload = { ...form }; delete payload.Id; await addGateway(payload); ElMessage.success('添加成功') }
|
||||
dialogVisible.value = false; fetchList()
|
||||
} catch {} finally { saving.value = false }
|
||||
}
|
||||
|
||||
// ---------- 删除 ----------
|
||||
async function onDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除网关「${row.Name}」吗?该网关下设备的网关关联将被清除。`, '提示', { type: 'warning' })
|
||||
try { await deleteGateway(row.Id); ElMessage.success('删除成功'); fetchList() } catch {}
|
||||
}
|
||||
|
||||
// ---------- 详情 ----------
|
||||
const detailVisible = ref(false)
|
||||
const detailTab = ref('info')
|
||||
const detailRow = ref(null)
|
||||
const gatewayDevices = ref([])
|
||||
const devicesLoading = ref(false)
|
||||
|
||||
function openDetail(row) {
|
||||
detailRow.value = row
|
||||
detailTab.value = 'info'
|
||||
detailVisible.value = true
|
||||
fetchGatewayDevices(row.Id)
|
||||
}
|
||||
|
||||
async function fetchGatewayDevices(gatewayId) {
|
||||
devicesLoading.value = true
|
||||
try {
|
||||
// 复用设备列表接口,按 gatewayId 筛选(需后端支持)
|
||||
gatewayDevices.value = (await getDeviceList({ pageIndex: 1, pageSize: 200, gatewayId })) || []
|
||||
} catch { gatewayDevices.value = [] } finally { devicesLoading.value = false }
|
||||
}
|
||||
|
||||
const fmtTime = (t) => t ? String(t).replace('T', ' ').slice(0, 19) : '-'
|
||||
|
||||
onMounted(() => fetchList())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.search-bar { display: flex; gap: 10px; margin-bottom: 16px; }
|
||||
.pagination { margin-top: 16px; display: flex; justify-content: flex-end; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user