625 lines
30 KiB
Vue
625 lines
30 KiB
Vue
<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>
|