fastbee/yack-web/src/views/platform/device-mapping/index.vue
2026-06-08 09:01:36 +08:00

625 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="platform-device-mapping">
<!-- 搜索 -->
<el-card v-show="showSearch" style="margin-bottom:10px">
<el-form :model="queryParams" ref="queryForm" size="mini" :inline="true" style="margin-bottom:-18px">
<el-form-item label="平台" prop="platformId">
<el-select v-model="queryParams.platformId" placeholder="全部平台" clearable style="width:140px" @change="handleQuery">
<el-option v-for="p in platformList" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
</el-form-item>
<el-form-item label="设备类型" prop="sensorType">
<el-select v-model="queryParams.sensorType" placeholder="全部" clearable filterable allow-create default-first-option style="width:140px">
<el-option v-for="s in sensorTypeOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="入网状态" prop="netStatus">
<el-select v-model="queryParams.netStatus" placeholder="全部" clearable style="width:100px">
<el-option label="已入网" :value="1" />
<el-option label="未入网" :value="0" />
</el-select>
</el-form-item>
<el-form-item style="float:right">
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<el-row :gutter="10" style="margin-bottom:15px">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['platform:deviceMapping:add']">新增绑定</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
</el-row>
<el-table v-loading="loading" :data="list" :row-key="row => row.id">
<el-table-column type="expand" width="30">
<template slot-scope="scope">
<div v-if="scope.row.deviceType === 1 && getSubDevicePairs(scope.row).length > 0" style="padding:10px 50px">
<el-table :data="getSubDevicePairs(scope.row)" size="mini" border>
<el-table-column label="子设备序列号" prop="subSerialNumber" min-width="140" />
<el-table-column label="角色" prop="deviceRole" width="80">
<template slot-scope="s">
<el-tag size="mini" :type="s.row.deviceRole === 'reference' ? 'warning' : ''">{{ s.row.deviceRole === 'reference' ? '基准' : '监测' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="平台设备ID" prop="platformDeviceId" min-width="200" show-overflow-tooltip />
<el-table-column label="平台测点ID" prop="platformPointId" min-width="200" show-overflow-tooltip />
<el-table-column label="入网" prop="netStatus" width="70" align="center">
<template slot-scope="s">
<el-tag size="mini" :type="s.row.netStatus === 1 ? 'success' : 'info'">{{ s.row.netStatus === 1 ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<div v-else style="padding:10px 50px;color:#909399;font-size:12px">无子设备映射</div>
</template>
</el-table-column>
<el-table-column label="平台" width="120">
<template slot-scope="scope">{{ platformName(scope.row.platformId) }}</template>
</el-table-column>
<el-table-column label="设备名称" min-width="140">
<template slot-scope="scope">{{ deviceName(scope.row.deviceId) }}</template>
</el-table-column>
<el-table-column label="序列号" prop="serialNumber" width="140" />
<el-table-column label="传感器类型" width="110" align="center">
<template slot-scope="scope">
<el-tag size="small">{{ sensorLabel(scope.row.sensorType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="设备类型" width="90" align="center">
<template slot-scope="scope">
<el-tag size="small" :type="scope.row.deviceType === 1 ? 'warning' : ''">{{ scope.row.deviceType === 1 ? '网关' : '直连' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="平台设备ID" width="200" show-overflow-tooltip>
<template slot-scope="scope">
<span v-if="scope.row.deviceType === 1">
<el-tag size="small" type="warning">子表({{ getSubDevicePairs(scope.row).length }})</el-tag>
</span>
<span v-else>{{ scope.row.platformDeviceId }}</span>
</template>
</el-table-column>
<el-table-column label="入网状态" width="90" align="center">
<template slot-scope="scope">
<el-tag size="small" type="success" v-if="scope.row.netStatus===1">已入网</el-tag>
<el-tag size="small" type="info" v-else>未入网</el-tag>
</template>
</el-table-column>
<el-table-column label="启用" width="70" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.enable" active-value="1" inactive-value="0" @change="handleEnableChange(scope.row)" />
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="200" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-connection" @click="handleOnboard(scope.row)" v-hasPermi="['platform:deviceMapping:edit']"
:disabled="scope.row.netStatus===1">入网</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['platform:deviceMapping:edit']">编辑</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['platform:deviceMapping:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog :title="title" :visible.sync="open" width="820px" append-to-body :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="110px">
<el-form-item label="所属平台" prop="platformId">
<el-select v-model="form.platformId" placeholder="请选择平台" style="width:100%" @change="onPlatformChange">
<el-option v-for="p in platformList" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
</el-form-item>
<el-form-item label="系统设备" prop="deviceId">
<el-select v-model="form.deviceId" placeholder="输入设备名称/序列号搜索" filterable remote
:remote-method="searchDevice" :loading="deviceSearchLoading" style="width:100%"
@change="onDeviceChange">
<el-option v-for="d in deviceOptions" :key="d.deviceId" :label="`${d.deviceName}(${d.serialNumber})`" :value="d.deviceId" />
</el-select>
</el-form-item>
<el-form-item label="序列号">
<el-input v-model="form.serialNumber" disabled />
</el-form-item>
<el-form-item label="传感器类型" prop="sensorType">
<el-select v-model="form.sensorType" placeholder="选择或输入设备类型" filterable allow-create default-first-option style="width:100%">
<el-option v-for="s in sensorTypeOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="设备类型" prop="deviceType">
<el-radio-group v-model="form.deviceType" @change="onDeviceTypeChange">
<el-radio :label="0">直连设备</el-radio>
<el-radio :label="1">网关设备(多子设备)</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="推送接口" prop="pushInterfaceId">
<el-select v-model="form.pushInterfaceId" placeholder="选择该平台的推送接口" style="width:100%" @change="onPushInterfaceChange" clearable>
<el-option v-for="iface in interfaceOptions" :key="iface.id" :label="`${iface.name}(${iface.method} ${iface.path})`" :value="iface.id" />
</el-select>
</el-form-item>
<!-- 直连设备:平台ID 字段 -->
<template v-if="form.deviceType !== 1">
<el-form-item label="平台设备ID">
<el-input v-model="form.platformDeviceId" placeholder="入网后自动回填,也可手动填写" />
</el-form-item>
<el-form-item label="平台测点ID">
<el-input v-model="form.platformPointId" placeholder="入网后自动回填,也可手动填写" />
</el-form-item>
</template>
<!-- 网关设备:子设备映射配置 -->
<div v-if="form.deviceType === 1" class="array-fields-section">
<div class="section-label">子设备映射</div>
<div class="schema-tip" style="margin-bottom:8px">
配置网关下参与上报的子设备,指定基准点和监测点角色。入网时每个监测点独立创建测点和绑定。
</div>
<el-table :data="subMappingRows" size="mini" border style="width:100%">
<el-table-column label="子设备编号" min-width="160">
<template slot-scope="scope">
<el-select v-if="subDeviceOptions.length > 0"
v-model="scope.row.subSerialNumber" size="mini" placeholder="选择子设备" filterable style="width:100%"
@change="onSubSerialChange(scope.row)">
<el-option v-for="device in subDeviceOptions" :key="device.serialNumber" :label="device.serialNumber" :value="device.serialNumber" />
</el-select>
<el-input v-else v-model="scope.row.subSerialNumber" size="mini" placeholder="填写子设备编号" @change="onSubSerialChange(scope.row)" />
</template>
</el-table-column>
<el-table-column label="角色" width="120" align="center">
<template slot-scope="scope">
<el-select v-model="scope.row.deviceRole" size="mini" style="width:100%">
<el-option label="基准点" value="reference" />
<el-option label="监测点" value="monitor" />
</el-select>
</template>
</el-table-column>
<el-table-column label="入网参数 (JSON)" min-width="220">
<template slot-scope="scope">
<el-input v-model="scope.row.onboardConfig" size="mini" :placeholder="scope.row.deviceRole === 'reference' ? '{\"sensorId\":\"...\"}' : '{\"sensorId\":\"...\",\"refSensorId\":\"...\",\"refSerialNumber\":\"...\"}'" />
</template>
</el-table-column>
<el-table-column label="平台设备ID" width="140" v-if="form.netStatus===1">
<template slot-scope="scope">
<span style="font-size:12px;color:#606266">{{ scope.row.platformDeviceId || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="50" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="subMappingRows.splice(scope.$index, 1)" />
</template>
</el-table-column>
</el-table>
<el-button size="mini" icon="el-icon-plus" style="margin-top:8px" @click="addSubMapping">添加子设备</el-button>
<!-- 数据映射配置 -->
<el-form-item label="上报间隔" label-width="100px" style="margin-top:12px;margin-bottom:0">
<el-input-number v-model="form.reportIntervalSec" :min="10" :max="3600" :step="10" style="width:160px" /> 秒
</el-form-item>
<el-form-item v-if="itemFieldMappingRows.length > 0" label="数组项映射" label-width="100px" style="margin-top:12px;margin-bottom:0">
<el-table :data="itemFieldMappingRows" size="mini" border style="width:100%">
<el-table-column label="平台字段" width="140">
<template slot-scope="scope">{{ scope.row.platformField }}</template>
</el-table-column>
<el-table-column label="物模型标识" min-width="200">
<template slot-scope="scope">
<el-select v-model="scope.row.identifier" size="mini" placeholder="选择物模型字段" filterable style="width:100%">
<el-option v-for="tm in uniqueThingsModelOptions" :key="tm.identifier" :label="`${tm.modelName}(${tm.identifier})`" :value="tm.identifier" />
</el-select>
</template>
</el-table-column>
</el-table>
</el-form-item>
</div>
<!-- 标量字段映射 -->
<el-form-item v-if="fieldMappingRows.length > 0" label="字段映射">
<el-table :data="fieldMappingRows" size="mini" border style="width:100%">
<el-table-column label="物模型标识" min-width="180">
<template slot-scope="scope">
<el-select v-model="scope.row.identifier" size="mini" placeholder="选择物模型字段" filterable style="width:100%">
<el-option v-for="tm in uniqueThingsModelOptions" :key="tm.identifier" :label="`${tm.modelName}(${tm.identifier})`" :value="tm.identifier" />
</el-select>
</template>
</el-table-column>
<el-table-column label="平台字段" min-width="160">
<template slot-scope="scope">
<el-input v-model="scope.row.platformField" size="mini" placeholder="平台字段名" />
</template>
</el-table-column>
<el-table-column label="操作" width="60" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" @click="fieldMappingRows.splice(scope.$index,1)" />
</template>
</el-table-column>
</el-table>
<el-button size="mini" icon="el-icon-plus" style="margin-top:8px" @click="addFieldMapping">添加字段</el-button>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="submitLoading">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listPlatformDeviceMapping, addPlatformDeviceMapping, updatePlatformDeviceMapping, delPlatformDeviceMapping, onboardDevice, listSubMappings, syncSubMappings } from '@/api/platform/deviceMapping'
import { listPlatformInstance } from '@/api/platform/instance'
import { listPlatformInterface } from '@/api/platform/interface'
import { listDevice, listThingsModel, getDevice } from '@/api/iot/device'
import { listGateway } from '@/api/iot/gateway'
const DEFAULT_SENSOR_TYPES = [
{ value: 'inclino', label: '倾角' },
{ value: 'stress', label: '轴力计' },
{ value: 'hyeto', label: '雨量计' },
{ value: 'static_level', label: '静力水准' }
]
export default {
name: 'PlatformDeviceMapping',
data() {
return {
loading: false, submitLoading: false, showSearch: true,
total: 0, list: [], platformList: [], deviceOptions: [], deviceMap: {},
subDeviceOptions: [],
deviceSearchLoading: false, thingsModelOptions: [],
interfaceOptions: [],
open: false, title: '',
fieldMappingRows: [],
// 网关子设备映射行
subMappingRows: [],
// 网关数据映射
itemFieldMappingRows: [],
// 子表缓存 (列表展开行使用)
subMappingCache: {},
queryParams: { pageNum: 1, pageSize: 20, platformId: null, sensorType: '', netStatus: null },
form: { enable: '1', deviceType: 0, reportIntervalSec: 300 },
rules: {
platformId: [{ required: true, message: '请选择平台', trigger: 'change' }],
deviceId: [{ required: true, message: '请选择设备', trigger: 'change' }],
sensorType: [{ required: true, message: '请选择或输入设备类型', trigger: 'change' }],
pushInterfaceId: [{ required: true, message: '请选择推送接口', trigger: 'change' }]
}
}
},
computed: {
uniqueThingsModelOptions() {
const deduped = []
const seen = new Set()
;(this.thingsModelOptions || []).forEach(item => {
if (!item || !item.identifier || seen.has(item.identifier)) return
seen.add(item.identifier)
deduped.push(item)
})
return deduped
},
sensorTypeOptions() {
const optionMap = new Map()
DEFAULT_SENSOR_TYPES.forEach(item => optionMap.set(item.value, { ...item }))
;(this.list || []).forEach(item => {
if (item && item.sensorType && !optionMap.has(item.sensorType)) {
optionMap.set(item.sensorType, { value: item.sensorType, label: item.sensorType })
}
})
return Array.from(optionMap.values())
}
},
created() {
listPlatformInstance({ pageNum: 1, pageSize: 100 }).then(res => { this.platformList = res.rows || [] })
this.getList()
},
methods: {
getList() {
this.loading = true
listPlatformDeviceMapping(this.queryParams).then(res => {
this.list = res.rows; this.total = res.total; this.loading = false
// 加载设备名称映射
const ids = [...new Set(res.rows.map(r => r.deviceId))]
if (ids.length) {
listDevice({ pageNum: 1, pageSize: 200 }).then(dr => {
const map = {}
dr.rows.forEach(d => { map[d.deviceId] = d.deviceName })
this.deviceMap = map
})
}
// 预加载网关设备的子映射
this.subMappingCache = {}
res.rows.filter(r => r.deviceType === 1).forEach(row => {
listSubMappings(row.id).then(subRes => {
this.$set(this.subMappingCache, row.id, subRes.data || [])
})
})
})
},
platformName(id) { const p = this.platformList.find(x => x.id === id); return p ? p.name : id },
deviceName(id) { return this.deviceMap[id] || id },
sensorLabel(v) {
const option = this.sensorTypeOptions.find(item => item.value === v)
return option ? option.label : v
},
getSubDevicePairs(row) {
return this.subMappingCache[row.id] || []
},
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
handleAdd() {
this.form = { enable: '1', deviceType: 0, pushInterfaceId: null, reportIntervalSec: 300 }
this.fieldMappingRows = []
this.subMappingRows = []
this.itemFieldMappingRows = []
this.deviceOptions = []
this.subDeviceOptions = []
this.thingsModelOptions = []
this.interfaceOptions = []
this.title = '新增设备绑定'; this.open = true
},
handleUpdate(row) {
this.form = { ...row, reportIntervalSec: 300 }
this.fieldMappingRows = this.safeParseJson(row.fieldMapping, [])
this.subMappingRows = []
this.itemFieldMappingRows = []
// 解析 extra_mapping 中的数据映射配置
if (row.extraMapping) {
try {
const extra = JSON.parse(row.extraMapping)
this.form.reportIntervalSec = extra.reportIntervalSec || 300
this.itemFieldMappingRows = extra.itemFieldMappings || []
} catch (e) { /* ignore */ }
}
// 加载该平台的接口列表
if (row.platformId) {
this.loadInterfaces(row.platformId)
}
// 加载子设备映射(网关设备)
if (row.deviceType === 1 && row.id) {
listSubMappings(row.id).then(res => {
this.subMappingRows = (res.data || []).map(s => ({ ...s }))
})
}
this.loadDeviceContext(row.deviceId)
this.title = '编辑设备绑定'; this.open = true
},
handleDelete(row) {
this.$confirm('确认删除?').then(() => {
delPlatformDeviceMapping(row.id).then(() => { this.getList(); this.$modal.msgSuccess('删除成功') })
})
},
handleEnableChange(row) {
updatePlatformDeviceMapping({ id: row.id, enable: row.enable })
},
handleOnboard(row) {
this.$confirm(`确认对设备【${this.deviceName(row.deviceId)}】执行入网操作?`).then(() => {
onboardDevice(row.id).then(() => { this.$modal.msgSuccess('入网成功'); this.getList() })
})
},
onPlatformChange(platformId) {
this.interfaceOptions = []
this.form.pushInterfaceId = null
if (platformId) this.loadInterfaces(platformId)
},
onDeviceTypeChange(val) {
if (val === 1 && this.subMappingRows.length === 0 && this.subDeviceOptions.length > 0) {
// 切换为网关设备时,自动用已加载的子设备列表预填
const refSn = this.subDeviceOptions[0].serialNumber
this.subMappingRows = this.subDeviceOptions.map((device, idx) => {
const role = idx === 0 ? 'reference' : 'monitor'
const config = role === 'reference'
? JSON.stringify({ sensorId: device.serialNumber })
: JSON.stringify({ sensorId: device.serialNumber, refSensorId: refSn, refSerialNumber: refSn })
return {
subSerialNumber: device.serialNumber,
deviceRole: role,
onboardConfig: config,
platformDeviceId: '',
platformPointId: ''
}
})
}
},
loadInterfaces(platformId) {
listPlatformInterface({ platformId, pageNum: 1, pageSize: 100 }).then(res => {
this.interfaceOptions = res.rows || []
})
},
onPushInterfaceChange(interfaceId) {
// 从推送接口的 requestSchema 提取字段作为数据映射模板
if (!interfaceId) return
const iface = this.interfaceOptions.find(x => x.id === interfaceId)
if (!iface || !iface.requestSchema) return
try {
const schema = JSON.parse(iface.requestSchema)
// 网关设备:提取 array 类型字段的 itemSchema 作为数组项映射模板
const arrayField = schema.find(f => f.type === 'array' && f.itemSchema && f.itemSchema.length > 0)
if (arrayField && this.form.deviceType === 1) {
const identifierFields = (arrayField.itemSchema || []).filter(s => s.source === 'thingsModel' || s.source === 'identifier')
if (identifierFields.length > 0 && this.itemFieldMappingRows.length === 0) {
this.itemFieldMappingRows = identifierFields.map(s => ({ platformField: s.name, identifier: '' }))
}
}
// 直连设备:提取标量字段(非 array)作为字段映射模板
if (this.form.deviceType !== 1) {
const scalarFields = schema.filter(f =>
f.type !== 'array' && (f.source === 'thingsModel' || f.source === 'identifier')
)
if (scalarFields.length > 0 && this.fieldMappingRows.length === 0) {
this.fieldMappingRows = scalarFields.map(s => ({ identifier: '', platformField: s.name }))
}
}
} catch (e) { /* ignore */ }
},
onDeviceChange(deviceId) {
const d = this.deviceOptions.find(x => x.deviceId === deviceId)
if (d) {
this.form.serialNumber = d.serialNumber
this.loadSubDeviceOptions(d)
} else {
this.subDeviceOptions = []
}
this.loadThingsModel(deviceId)
},
searchDevice(query) {
if (!query) return
this.deviceSearchLoading = true
listDevice({ pageNum: 1, pageSize: 50, deviceName: query }).then(res => {
this.deviceOptions = res.rows; this.deviceSearchLoading = false
})
},
loadDeviceContext(deviceId) {
if (!deviceId) return
getDevice(deviceId).then(res => {
const device = res.data || res
if (!device) return
this.deviceOptions = [{ deviceId: device.deviceId, deviceName: device.deviceName, serialNumber: device.serialNumber }]
this.form.deviceId = device.deviceId
this.form.serialNumber = device.serialNumber
this.loadThingsModel(device.deviceId)
this.loadSubDeviceOptions(device)
}).catch(() => {
this.loadThingsModel(deviceId)
})
},
loadSubDeviceOptions(gateway) {
const gatewayDeviceId = gateway.deviceId
const gatewaySerialNumber = gateway.serialNumber
if (!gatewayDeviceId && !gatewaySerialNumber) {
this.subDeviceOptions = []
return
}
const promises = []
if (gatewayDeviceId) {
promises.push(listGateway({ gwDeviceId: gatewayDeviceId, pageNum: 1, pageSize: 500 }).then(res => res.rows || []).catch(() => []))
}
if (gatewaySerialNumber) {
promises.push(listDevice({ gwDevCode: gatewaySerialNumber, pageNum: 1, pageSize: 500 }).then(res => res.rows || []).catch(() => []))
}
Promise.all(promises).then(results => {
const optionMap = new Map()
results.forEach(rows => {
;(rows || []).forEach(item => {
const sn = (item.serialNumber || item.subDeviceNo || '').trim()
if (sn && !optionMap.has(sn)) {
optionMap.set(sn, { serialNumber: sn, deviceName: item.deviceName || item.subDeviceName || sn })
}
})
})
this.subDeviceOptions = Array.from(optionMap.values())
// 网关设备且子映射为空时,自动预填所有子设备,第一个为基准点
if (this.form.deviceType === 1 && this.subMappingRows.length === 0 && this.subDeviceOptions.length > 0) {
this.subMappingRows = this.subDeviceOptions.map((device, idx) => {
const role = idx === 0 ? 'reference' : 'monitor'
const refSn = this.subDeviceOptions[0].serialNumber
const config = role === 'reference'
? JSON.stringify({ sensorId: device.serialNumber })
: JSON.stringify({ sensorId: device.serialNumber, refSensorId: refSn, refSerialNumber: refSn })
return {
subSerialNumber: device.serialNumber,
deviceRole: role,
onboardConfig: config,
platformDeviceId: '',
platformPointId: ''
}
})
}
})
},
loadThingsModel(deviceId) {
if (!deviceId) return
listThingsModel({ deviceId, pageNum: 1, pageSize: 200 }).then(res => {
this.thingsModelOptions = res.rows || []
})
},
addFieldMapping() { this.fieldMappingRows.push({ identifier: '', platformField: '' }) },
addSubMapping() {
this.subMappingRows.push({ subSerialNumber: '', deviceRole: 'monitor', onboardConfig: '', platformDeviceId: '', platformPointId: '' })
},
onSubSerialChange(row) {
// 自动填充 onboardConfig 中的 sensorId
if (!row.onboardConfig && row.subSerialNumber) {
const refRow = this.subMappingRows.find(r => r.deviceRole === 'reference')
if (row.deviceRole === 'reference') {
row.onboardConfig = JSON.stringify({ sensorId: row.subSerialNumber })
} else if (refRow) {
row.onboardConfig = JSON.stringify({ sensorId: row.subSerialNumber, refSensorId: refRow.subSerialNumber, refSerialNumber: refRow.subSerialNumber })
} else {
row.onboardConfig = JSON.stringify({ sensorId: row.subSerialNumber })
}
}
},
safeParseJson(value, defaultValue) {
if (!value) return defaultValue
try { return JSON.parse(value) } catch (e) { return defaultValue }
},
cancel() { this.open = false },
submitForm() {
this.$refs.form.validate(valid => {
if (!valid) return
this.form.fieldMapping = this.fieldMappingRows.length > 0
? JSON.stringify(this.fieldMappingRows) : null
// 网关设备:extra_mapping 只存数据映射配置
if (this.form.deviceType === 1) {
this.form.extraMapping = JSON.stringify({
reportIntervalSec: this.form.reportIntervalSec || 300,
itemFieldMappings: this.itemFieldMappingRows.filter(im => im.identifier)
})
} else {
this.form.extraMapping = null
}
this.submitLoading = true
const api = this.form.id ? updatePlatformDeviceMapping : addPlatformDeviceMapping
api(this.form).then(res => {
const mappingId = this.form.id || (res.data && res.data.id) || res.data
// 网关设备:保存后同步子映射
if (this.form.deviceType === 1 && mappingId) {
const subData = this.subMappingRows
.filter(r => r.subSerialNumber)
.map(r => ({
subSerialNumber: r.subSerialNumber,
deviceRole: r.deviceRole || 'monitor',
onboardConfig: r.onboardConfig || null,
platformDeviceId: r.platformDeviceId || null,
platformPointId: r.platformPointId || null
}))
syncSubMappings(mappingId, subData).then(() => {
this.$modal.msgSuccess(this.form.id ? '修改成功' : '新增成功')
this.open = false; this.getList()
})
} else {
this.$modal.msgSuccess(this.form.id ? '修改成功' : '新增成功')
this.open = false; this.getList()
}
}).finally(() => { this.submitLoading = false })
})
}
}
}
</script>
<style scoped>
.schema-tip {
font-size: 12px;
color: #909399;
line-height: 1.6;
}
.array-fields-section {
margin-bottom: 22px;
padding: 0 10px;
}
.array-fields-section .section-label {
font-size: 14px;
color: #606266;
font-weight: 600;
margin-bottom: 8px;
}
</style>