20260309:设备批量新增

This commit is contained in:
George 2026-03-09 23:18:35 +08:00
parent 2cfc9c35ea
commit 9536ae19b1
12 changed files with 652 additions and 13 deletions

View File

@ -21,6 +21,7 @@ import com.fastbee.iot.model.DeviceImportVO;
import com.fastbee.iot.model.DeviceRelateUserInput;
import com.fastbee.iot.model.SerialNumberVO;
import com.fastbee.iot.model.dto.ThingsModelDTO;
import com.fastbee.iot.model.gateWay.SubDeviceCreateByProductVO;
import com.fastbee.iot.service.IDeviceService;
import com.github.pagehelper.PageInfo;
import io.swagger.annotations.Api;
@ -493,4 +494,15 @@ public class DeviceController extends BaseController {
}
}
/**
* 根据子产品批量新增子设备
*/
@PreAuthorize("@ss.hasPermi('iot:device:add')")
@Log(title = "批量新增子设备", businessType = BusinessType.INSERT)
@PostMapping("/addBatchByProduct")
@ApiOperation("根据子产品批量新增子设备")
public AjaxResult addBatchByProduct(@RequestBody SubDeviceCreateByProductVO vo) {
int count = deviceService.addSubDevicesByProducts(vo);
return AjaxResult.success("成功新增 " + count + " 个子设备");
}
}

View File

@ -16,6 +16,7 @@ import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import static com.fastbee.common.exception.ServiceExceptionUtil.exception;
@ -43,7 +44,7 @@ public class BaiduILocationService implements ILocationService {
log.info("结构化地址:{}", address);
String baseUrl = FastBeeConstant.MAP.BAIDU_GEOCODING;
address = StringUtils.isEmpty(address) ? "湖南省长沙市岳麓区" : address;
address = StringUtils.isEmpty(address) || Objects.equals("内网IP", address) ? "湖南省长沙市岳麓区" : address;
String encodedAddress;
try {
encodedAddress = URLEncoder.encode(address, StandardCharsets.UTF_8.toString());

View File

@ -0,0 +1,36 @@
package com.fastbee.iot.model.gateWay;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
* 根据子产品批量新增子设备VO
*/
@Data
public class SubDeviceCreateByProductVO {
@ApiModelProperty("网关设备id")
private Long gwDeviceId;
@ApiModelProperty("子产品设备创建项")
private List<SubProductDeviceItem> items;
@Data
public static class SubProductDeviceItem {
@ApiModelProperty("子产品id")
private Long subProductId;
@ApiModelProperty("子设备详情列表")
private List<DeviceDetail> devices;
}
@Data
public static class DeviceDetail {
@ApiModelProperty("自定义设备名称")
private String deviceName;
@ApiModelProperty("自定义从机地址")
private Integer slaveId;
}
}

View File

@ -51,4 +51,10 @@ public class SubDeviceListVO {
private Date createTime;
private Long subProductId;
@ApiModelProperty("子产品名称")
private String subProductName;
@ApiModelProperty("在线状态")
private Integer status;
}

View File

@ -10,6 +10,7 @@ import com.fastbee.iot.model.*;
import com.fastbee.iot.model.ThingsModels.ThingsModelShadow;
import com.fastbee.common.core.thingsModel.ThingsModelValuesInput;
import com.fastbee.iot.model.dto.ThingsModelDTO;
import com.fastbee.iot.model.gateWay.SubDeviceCreateByProductVO;
import org.quartz.SchedulerException;
import java.util.List;
@ -146,6 +147,14 @@ public interface IDeviceService {
*/
public Device insertDevice(Device device);
/**
* 根据子产品批量新增子设备并绑定到网关
*
* @param vo 包含网关设备id和子产品设备创建项列表
* @return 新增的子设备数量
*/
public int addSubDevicesByProducts(SubDeviceCreateByProductVO vo);
/**
* 设备关联用户
*

View File

@ -43,6 +43,7 @@ import com.fastbee.iot.model.ThingsModels.ThingsModelValueItem;
import com.fastbee.iot.model.ThingsModels.ValueItem;
import com.fastbee.iot.model.dto.ThingsModelDTO;
import com.fastbee.iot.model.gateWay.ProductSubGatewayVO;
import com.fastbee.iot.model.gateWay.SubDeviceCreateByProductVO;
import com.fastbee.iot.model.gateWay.SubDeviceListVO;
import com.fastbee.iot.service.*;
import com.fastbee.iot.tdengine.service.ILogService;
@ -1189,6 +1190,109 @@ public class DeviceServiceImpl implements IDeviceService {
}
}
/**
* 根据子产品批量新增子设备并绑定到网关(支持自定义每个设备的名称和从机地址)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int addSubDevicesByProducts(SubDeviceCreateByProductVO vo) {
Long gwDeviceId = vo.getGwDeviceId();
List<SubDeviceCreateByProductVO.SubProductDeviceItem> items = vo.getItems();
if (items == null || items.isEmpty()) {
throw new ServiceException("子产品创建项不能为空");
}
Device gwDevice = deviceMapper.selectDeviceByDeviceId(gwDeviceId);
if (gwDevice == null) {
throw new ServiceException("网关设备不存在");
}
Product product = productService.selectProductByProductId(gwDevice.getProductId());
if (product == null) {
throw new ServiceException("网关产品不存在");
}
List<Long> subProductIdList = items.stream()
.map(SubDeviceCreateByProductVO.SubProductDeviceItem::getSubProductId)
.collect(Collectors.toList());
Map<Long, List<SubDeviceCreateByProductVO.DeviceDetail>> deviceDetailMap = items.stream()
.collect(Collectors.toMap(
SubDeviceCreateByProductVO.SubProductDeviceItem::getSubProductId,
i -> i.getDevices() != null ? i.getDevices() : Collections.emptyList()));
ProductSubGateway query = new ProductSubGateway();
query.setGwProductId(product.getProductId());
List<ProductSubGatewayVO> allSubProducts = productSubGatewayMapper.selectListVO(query);
List<ProductSubGatewayVO> selectedSubProducts = allSubProducts.stream()
.filter(p -> subProductIdList.contains(p.getSubProductId()))
.sorted(Comparator.comparing(ProductSubGatewayVO::getSlaveId))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(selectedSubProducts)) {
throw new ServiceException("未找到匹配的子产品");
}
List<Long> productIds = selectedSubProducts.stream()
.map(ProductSubGatewayVO::getSubProductId).collect(Collectors.toList());
LambdaQueryWrapper<ProductModbusJob> jobQuery = new LambdaQueryWrapper<>();
jobQuery.in(ProductModbusJob::getProductId, productIds);
List<ProductModbusJob> productModbusJobList = productModbusJobMapper.selectList(jobQuery);
Map<Long, List<ProductModbusJob>> productModbusJobMap = productModbusJobList.stream()
.collect(Collectors.groupingBy(ProductModbusJob::getProductId));
int totalCount = 0;
for (ProductSubGatewayVO subProduct : selectedSubProducts) {
List<SubDeviceCreateByProductVO.DeviceDetail> details =
deviceDetailMap.getOrDefault(subProduct.getSubProductId(), Collections.emptyList());
if (details.isEmpty()) {
continue;
}
for (int i = 0; i < details.size(); i++) {
SubDeviceCreateByProductVO.DeviceDetail detail = details.get(i);
String deviceName = StringUtils.isNotEmpty(detail.getDeviceName())
? detail.getDeviceName()
: subProduct.getSubProductName() + "_" + subProduct.getSlaveId() + "_" + (i + 1);
Integer slaveId = detail.getSlaveId() != null
? detail.getSlaveId()
: subProduct.getSlaveId();
Device addDevice = new Device();
addDevice.setDeviceName(deviceName);
addDevice.setProductId(subProduct.getSubProductId());
addDevice.setProductName(subProduct.getSubProductName());
addDevice.setSerialNumber(this.generationDeviceNum(1));
addDevice.setTenantId(gwDevice.getTenantId());
addDevice.setTenantName(gwDevice.getTenantName());
addDevice.setCreateBy(SecurityUtils.getUsername());
addDevice.setFirmwareVersion(new BigDecimal(1));
addDevice.setRssi(0);
addDevice.setIsShadow(0);
addDevice.setLocationWay(subProduct.getSubLocationWay());
addDevice.setImgUrl(product.getImgUrl());
addDevice.setCreateTime(new Date());
deviceMapper.insertDevice(addDevice);
SubGateway subGateway = new SubGateway();
subGateway.setGwDeviceId(gwDeviceId);
subGateway.setSubDeviceId(addDevice.getDeviceId());
subGateway.setSlaveId(slaveId);
subGateway.setCreateBy(SecurityUtils.getUsername());
subGateway.setCreateTime(new Date());
subGatewayMapper.insertGateway(subGateway);
List<ProductModbusJob> modbusJobs = productModbusJobMap.get(subProduct.getSubProductId());
if (!CollectionUtils.isEmpty(modbusJobs)) {
for (ProductModbusJob pmj : modbusJobs) {
ModbusJob modbusJob = new ModbusJob();
modbusJob.setJobName(pmj.getJobName());
modbusJob.setSubDeviceId(addDevice.getDeviceId());
modbusJob.setSubSerialNumber(addDevice.getSerialNumber());
modbusJob.setCommand(pmj.getCommand());
modbusJob.setStatus(pmj.getStatus());
modbusJob.setRemark(pmj.getRemark());
modbusJobService.insertModbusJob(modbusJob);
if (FastBeeConstant.PROTOCOL.ModbusTcp.equals(product.getProtocolCode())) {
redisCache.getCacheModbusTcpId(gwDevice.getSerialNumber());
}
}
}
itslValueCache.addCacheDeviceStatus(addDevice.getProductId(), addDevice.getSerialNumber());
totalCount++;
}
}
return totalCount;
}
/**
* 终端-设备关联用户
*

View File

@ -30,6 +30,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="subDeviceNo" column="serial_number"/>
<result property="createTime" column="create_time" />
<result property="subProductId" column="sub_product_id"/>
<result property="subProductName" column="sub_product_name"/>
<result property="status" column="status"/>
<result property="id" column="id" />
</resultMap>
@ -47,8 +49,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</sql>
<select id="selectGatewayList" parameterType="com.fastbee.iot.domain.SubGateway" resultMap="GatewaySubDeviceListResult">
select ga.id, ga.gw_device_id, ga.sub_device_id, ga.slave_id, ga.create_time ,d.device_name, d.serial_number, d.product_id sub_product_id
from iot_sub_gateway ga inner join iot_device d on ga.sub_device_id = d.device_id
select ga.id, ga.gw_device_id, ga.sub_device_id, ga.slave_id, ga.create_time,
d.device_name, d.serial_number, d.product_id sub_product_id, d.status,
p.product_name as sub_product_name
from iot_sub_gateway ga
inner join iot_device d on ga.sub_device_id = d.device_id
left join iot_product p on d.product_id = p.product_id
<where>
<if test="gwDeviceId != null and gwDeviceId != 0">
gw_device_id = #{gwDeviceId}
@ -57,6 +63,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
sub_device_id = #{subDeviceId}
</if>
</where>
order by p.product_name, ga.slave_id
</select>
<select id="selectGatewayById" parameterType="Long" resultMap="GatewayResult">

View File

@ -70,6 +70,15 @@ export function editGatewayBatch(data) {
});
}
// 根据子产品批量新增子设备
export function createSubDeviceByProducts(data) {
return request({
url: '/iot/device/addBatchByProduct',
method: 'post',
data: data,
});
}
///产品///
// 查询网关与子产品关联列表
export function gatewayProductList(query) {

View File

@ -571,12 +571,30 @@
"device.running-status.866086-29": "The number of elements does not match, the number of array elements is",
"device.running-status.866086-30": "Number, separated by English commas.",
"device.running-status.866086-31": "Equipment upgrade",
"device.sub.083943-0": "Add sub devices",
"device.sub.binding": "Binding sub devices",
"device.sub.createByProduct": "Add Sub Devices",
"device.sub-device-create.title": "Add Sub Devices by Products",
"device.sub-device-create.productListTitle": "Sub Product List",
"device.sub-device-create.noSubProducts": "No sub products found. Please configure sub products in product management first.",
"device.sub-device-create.enabled": "Enabled",
"device.sub-device-create.deviceConfig": "Device Config",
"device.sub-device-create.deviceCount": "Device Count",
"device.sub-device-create.deviceListTitle": "Device List",
"device.sub-device-create.summary": "Selected {0} sub products, will create {1} sub devices",
"device.sub-device-create.devicePrefix": "Sub Device",
"device.sub-device-create.deviceNameLabel": "Device Name",
"device.sub-device-create.deviceNamePlaceholder": "Please enter device name",
"device.sub-device-create.slaveIdLabel": "Slave Address",
"device.sub-device-create.slaveIdPlaceholder": "Please enter slave address",
"device.sub-device-create.selectTip": "Please enable at least one sub product and set device count",
"device.sub-device-create.success": "Sub devices added successfully",
"device.sub.083943-1": "Remove sub devices",
"device.sub.083943-2": "Set sub device address",
"device.sub.083943-3": "Sub device address",
"device.sub.083943-4": "Are you sure to delete the data item with device number {0]?",
"device.sub.083943-5": "Save successful",
"device.sub.subProductName": "Sub Product",
"device.sub.onlineStatus": "Online Status",
"device.sub-device-list.323213-0": "Select device",
"device.sub-device-list.323213-1": "Please enter the sub device number",
"device.sub-device-list.323213-2": "name",

View File

@ -571,12 +571,30 @@
"device.running-status.866086-29": "元素个数不匹配,数组元素个数为",
"device.running-status.866086-30": "个,以英文逗号分隔。",
"device.running-status.866086-31": "设备升级",
"device.sub.083943-0": "添加子设备",
"device.sub.binding": "绑定子设备",
"device.sub.createByProduct": "新增子设备",
"device.sub-device-create.title": "根据子产品新增子设备",
"device.sub-device-create.productListTitle": "子产品列表",
"device.sub-device-create.noSubProducts": "当前网关产品下暂无子产品,请先在产品管理中配置子产品",
"device.sub-device-create.enabled": "已启用",
"device.sub-device-create.deviceConfig": "子设备配置",
"device.sub-device-create.deviceCount": "子设备数量",
"device.sub-device-create.deviceListTitle": "子设备列表",
"device.sub-device-create.summary": "已选择 {0} 个子产品,将新增 {1} 个子设备",
"device.sub-device-create.devicePrefix": "子设备",
"device.sub-device-create.deviceNameLabel": "子设备名称",
"device.sub-device-create.deviceNamePlaceholder": "请输入子设备名称",
"device.sub-device-create.slaveIdLabel": "从机地址",
"device.sub-device-create.slaveIdPlaceholder": "请输入从机地址",
"device.sub-device-create.selectTip": "请至少启用一个子产品并设置设备数量",
"device.sub-device-create.success": "新增子设备成功",
"device.sub.083943-1": "移除子设备",
"device.sub.083943-2": "设置子设备地址",
"device.sub.083943-3": "子设备地址",
"device.sub.083943-4": "是否确认删除设备编号为{0]的数据项?",
"device.sub.083943-5": "保存成功",
"device.sub.subProductName": "所属子产品",
"device.sub.onlineStatus": "在线状态",
"device.sub-device-list.323213-0": "选择设备",
"device.sub-device-list.323213-1": "请输入子设备编号",
"device.sub-device-list.323213-2": "名称",

View File

@ -2,7 +2,10 @@
<div class="device-sub-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item>
<el-button plain type="primary" icon="el-icon-plus" size="mini" @click="handleAdd">{{ $t('device.sub.083943-0') }}</el-button>
<el-button plain type="primary" icon="el-icon-plus" size="mini" @click="handleAdd">{{ $t('device.sub.binding') }}</el-button>
</el-form-item>
<el-form-item>
<el-button plain type="success" icon="el-icon-plus" size="mini" @click="handleCreateByProduct">{{ $t('device.sub.createByProduct') }}</el-button>
</el-form-item>
<el-form-item>
<el-button plain type="danger" icon="el-icon-delete" size="mini" @click="handleDelete" :disabled="this.ids.length == 0">{{ $t('device.sub.083943-1') }}</el-button>
@ -22,8 +25,13 @@
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="deviceList" @selection-change="handleSelectionChange" :border="false">
<el-table v-loading="loading" :data="deviceList" @selection-change="handleSelectionChange" :border="true" :span-method="mergeProductRows">
<el-table-column type="selection" width="55" align="center" />
<el-table-column :label="$t('device.sub.subProductName')" align="center" prop="subProductName" min-width="120">
<template slot-scope="scope">
<el-tag size="small" effect="plain">{{ scope.row.subProductName || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('device.device-edit.148398-1')" align="center" prop="subDeviceName" />
<el-table-column :label="$t('device.device-edit.148398-7')" align="center" prop="subDeviceNo" />
<el-table-column :label="$t('device.sub.083943-3')" align="center" prop="slaveId" width="180px">
@ -31,22 +39,31 @@
<el-input style="width: 120px; text-align: center" :disabled="!isSet" v-model="scope.row.slaveId" size="small" :placeholder="$t('device.sub.083943-2')"></el-input>
</template>
</el-table-column>
<!-- <el-table-column label="在线状态" align="center" prop="status">-->
<!-- <template slot-scope="scope">-->
<!-- <dict-tag :options="dict.type.iot_device_status" :value="scope.row.status" />-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column :label="$t('device.sub.onlineStatus')" align="center" prop="status">
<template slot-scope="scope">
<dict-tag :options="dict.type.iot_device_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column :label="$t('creatTime')" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('opation')" align="center" class-name="small-padding fixed-width" width="150">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="goDeviceDetail(scope.row.subDeviceId)">{{ $t('detail') }}</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">{{ $t('device.sub.083943-1') }}</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" />
<!-- 子设备列表 -->
<subDeviceList ref="subDeviceList" :gateway="gateway" @addSuccess="addSuccess"></subDeviceList>
<!-- 根据子产品新增子设备 -->
<subDeviceCreate ref="subDeviceCreate" :gateway="gateway" @addSuccess="addSuccess"></subDeviceCreate>
</div>
</template>
@ -54,6 +71,7 @@
import { listGateway, delGateway, editGatewayBatch } from '@/api/iot/gateway';
import subDeviceList from './sub-device-list';
import subDeviceCreate from './sub-device-create';
export default {
name: 'device-sub',
@ -65,6 +83,7 @@ export default {
},
components: {
subDeviceList,
subDeviceCreate,
},
dicts: ['iot_device_status'],
data() {
@ -98,6 +117,8 @@ export default {
gateway: {},
//是否可设置子设备地址
isSet: false,
// 子产品行合并信息
productSpans: [],
};
},
@ -117,13 +138,39 @@ export default {
methods: {
/** 查询子设备列表 */
getList() {
// this.loading = true;
listGateway(this.queryParams).then((response) => {
this.deviceList = response.rows;
this.total = response.total;
this.loading = false;
this.computeProductSpans();
});
},
computeProductSpans() {
const spans = [];
const list = this.deviceList || [];
let i = 0;
while (i < list.length) {
const productId = list[i].subProductId;
let count = 1;
while (i + count < list.length && list[i + count].subProductId === productId) {
count++;
}
for (let j = 0; j < count; j++) {
spans.push(j === 0 ? count : 0);
}
i += count;
}
this.productSpans = spans;
},
mergeProductRows({ row, column, rowIndex, columnIndex }) {
if (columnIndex === 1) {
const span = this.productSpans[rowIndex];
if (span > 0) {
return { rowspan: span, colspan: 1 };
}
return { rowspan: 0, colspan: 0 };
}
},
// 取消按钮
cancel() {
this.open = false;
@ -162,6 +209,14 @@ export default {
this.$refs.subDeviceList.openDeviceList = true;
this.$refs.subDeviceList.getList();
},
/** 根据子产品新增子设备 */
handleCreateByProduct() {
if (!this.device || !this.device.productId) {
this.$modal.msgWarning(this.$t('device.device-edit.148398-72'));
return;
}
this.$refs.subDeviceCreate.open(this.device.productId);
},
/** 删除按钮操作 */
handleDelete(row) {
const deviceIds = row.deviceId || this.ids;
@ -195,6 +250,15 @@ export default {
addSuccess() {
this.getList();
},
/** 跳转子设备详情页 */
goDeviceDetail(deviceId) {
if (!deviceId) return;
const route = this.$router.resolve({
path: '/iot/device-edit',
query: { deviceId: deviceId, isSubDev: 0, pageNum: 1, activeName: 'basic' },
});
window.open(route.href, '_blank');
},
},
};
</script>

View File

@ -0,0 +1,355 @@
<template>
<el-dialog
:title="$t('device.sub-device-create.title')"
:visible.sync="visible"
width="720px"
append-to-body
:close-on-click-modal="false"
@close="handleClose"
>
<div v-loading="loading" class="sub-device-create">
<div class="section-title">
<i class="el-icon-s-grid" style="margin-right: 4px"></i>
{{ $t('device.sub-device-create.productListTitle') }}
</div>
<div v-if="subProductList.length === 0 && !loading" class="empty-tip">
{{ $t('device.sub-device-create.noSubProducts') }}
</div>
<el-collapse v-model="activeNames">
<el-collapse-item
v-for="(item, index) in subProductList"
:key="item.subProductId"
:name="index"
>
<template slot="title">
<div class="product-header">
<i class="el-icon-cpu" style="color: #409eff; margin-right: 6px"></i>
<span class="product-name">{{ item.subProductName }}</span>
<el-tag v-if="item.enabled" size="mini" type="success" effect="plain" style="margin-left: 8px">
{{ $t('device.sub-device-create.enabled') }}
</el-tag>
<el-tag v-if="item.enabled" size="mini" effect="plain" style="margin-left: 4px">
x{{ item.devices.length }}
</el-tag>
</div>
</template>
<div class="product-body">
<el-row :gutter="24" class="config-row">
<el-col :span="10">
<div class="config-item">
<span class="config-label">{{ $t('device.sub-device-create.deviceConfig') }}</span>
<el-switch v-model="item.enabled" active-color="#409eff" @change="onEnableChange(index, $event)" />
</div>
</el-col>
<el-col :span="14">
<div class="config-item">
<span class="config-label">{{ $t('device.sub-device-create.deviceCount') }}</span>
<el-input-number
v-model="item.count"
:min="1"
:max="10"
:disabled="!item.enabled"
size="small"
@change="onCountChange(index)"
@input.native="onCountInput(index, $event)"
/>
</div>
</el-col>
</el-row>
<div v-if="item.enabled && item.devices.length > 0" class="device-list-section">
<div class="device-list-title">
<i class="el-icon-menu" style="margin-right: 4px; color: #409eff"></i>
{{ $t('device.sub-device-create.deviceListTitle') }}
</div>
<el-collapse v-model="item.expandedDevices" class="device-collapse">
<el-collapse-item
v-for="(dev, dIdx) in item.devices"
:key="dIdx"
:name="dIdx"
>
<template slot="title">
<div class="device-header">
<i class="el-icon-monitor" style="color: #909399; margin-right: 6px"></i>
<span>{{ dev.deviceName || ($t('device.sub-device-create.devicePrefix') + (dIdx + 1)) }}</span>
</div>
</template>
<div class="device-form">
<el-row :gutter="16">
<el-col :span="14">
<div class="device-form-item">
<span class="device-form-label">{{ $t('device.sub-device-create.deviceNameLabel') }}</span>
<el-input
v-model="dev.deviceName"
size="small"
:placeholder="$t('device.sub-device-create.deviceNamePlaceholder')"
/>
</div>
</el-col>
<el-col :span="10">
<div class="device-form-item">
<span class="device-form-label">{{ $t('device.sub-device-create.slaveIdLabel') }}</span>
<el-input-number
v-model="dev.slaveId"
:min="1"
:max="255"
size="small"
:placeholder="$t('device.sub-device-create.slaveIdPlaceholder')"
controls-position="right"
/>
</div>
</el-col>
</el-row>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</el-collapse-item>
</el-collapse>
<div v-if="totalDeviceCount > 0" class="summary-bar">
<i class="el-icon-info" style="color: #409eff; margin-right: 4px"></i>
{{ $t('device.sub-device-create.summary', [enabledProductCount, totalDeviceCount]) }}
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="submitting" :disabled="totalDeviceCount === 0" @click="handleConfirm">
{{ $t('confirm') }}
</el-button>
<el-button @click="handleClose">{{ $t('cancel') }}</el-button>
</div>
</el-dialog>
</template>
<script>
import { gatewayProductList, createSubDeviceByProducts } from '@/api/iot/gateway';
export default {
name: 'sub-device-create',
props: {
gateway: {
type: Object,
default: null,
},
},
data() {
return {
visible: false,
loading: false,
submitting: false,
subProductList: [],
activeNames: [],
};
},
computed: {
enabledProducts() {
return this.subProductList.filter((p) => p.enabled);
},
enabledProductCount() {
return this.enabledProducts.length;
},
totalDeviceCount() {
return this.enabledProducts.reduce((sum, p) => sum + (p.devices ? p.devices.length : 0), 0);
},
},
methods: {
open(productId) {
this.visible = true;
this.subProductList = [];
this.activeNames = [];
this.getSubProducts(productId);
},
getSubProducts(productId) {
this.loading = true;
gatewayProductList({ gwProductId: productId, pageNum: 1, pageSize: 100 })
.then((response) => {
const rows = response.rows || [];
this.subProductList = rows.map((item) => ({
...item,
enabled: false,
count: 1,
devices: [],
expandedDevices: [],
}));
this.activeNames = rows.map((_, idx) => idx);
})
.finally(() => {
this.loading = false;
});
},
onEnableChange(index, val) {
const item = this.subProductList[index];
if (val) {
this.rebuildDevices(index);
} else {
this.$set(item, 'devices', []);
this.$set(item, 'expandedDevices', []);
}
},
onCountChange(index) {
this.rebuildDevices(index);
},
onCountInput(index, e) {
this.$nextTick(() => {
this.rebuildDevices(index);
});
},
rebuildDevices(index) {
const item = this.subProductList[index];
if (!item.enabled) return;
const count = Math.max(1, Math.min(10, item.count || 1));
const baseSlaveId = item.slaveId || 1;
const oldDevices = item.devices || [];
const devices = [];
for (let i = 0; i < count; i++) {
if (i < oldDevices.length) {
devices.push(oldDevices[i]);
} else {
devices.push({
deviceName: item.subProductName + '_' + (i + 1),
slaveId: baseSlaveId + i,
});
}
}
this.$set(item, 'devices', devices);
this.$set(item, 'expandedDevices', devices.map((_, i) => i));
},
handleConfirm() {
if (this.totalDeviceCount === 0) {
this.$modal.msgWarning(this.$t('device.sub-device-create.selectTip'));
return;
}
const items = this.enabledProducts.map((p) => ({
subProductId: p.subProductId,
devices: (p.devices || []).map((d) => ({
deviceName: d.deviceName,
slaveId: d.slaveId,
})),
}));
this.submitting = true;
createSubDeviceByProducts({
gwDeviceId: this.gateway.gwDeviceId,
items: items,
})
.then((response) => {
this.$modal.msgSuccess(response.msg || this.$t('device.sub-device-create.success'));
this.visible = false;
this.$emit('addSuccess');
})
.finally(() => {
this.submitting = false;
});
},
handleClose() {
this.visible = false;
},
},
};
</script>
<style scoped>
.sub-device-create {
max-height: 520px;
overflow-y: auto;
}
.section-title {
font-weight: 600;
font-size: 15px;
color: #303133;
margin-bottom: 12px;
display: flex;
align-items: center;
}
.empty-tip {
text-align: center;
color: #909399;
padding: 40px 0;
font-size: 14px;
}
.product-header {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
}
.product-name {
color: #303133;
}
.product-body {
padding: 8px 0 4px;
}
.config-row {
margin-bottom: 12px;
}
.config-item {
display: flex;
align-items: center;
gap: 12px;
}
.config-label {
font-size: 13px;
color: #606266;
white-space: nowrap;
}
.device-list-section {
margin-top: 8px;
}
.device-list-title {
font-size: 13px;
font-weight: 500;
color: #303133;
margin-bottom: 6px;
display: flex;
align-items: center;
}
.device-collapse {
border: 1px solid #ebeef5;
border-radius: 4px;
}
.device-collapse >>> .el-collapse-item__header {
padding-left: 14px;
height: 40px;
line-height: 40px;
font-size: 13px;
background: #fafafa;
}
.device-collapse >>> .el-collapse-item__wrap {
border-bottom: 1px solid #f2f6fc;
}
.device-header {
display: flex;
align-items: center;
font-size: 13px;
color: #606266;
}
.device-form {
padding: 12px 14px 8px;
}
.device-form-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.device-form-label {
font-size: 12px;
color: #909399;
white-space: nowrap;
min-width: 56px;
}
.summary-bar {
margin-top: 16px;
padding: 10px 14px;
background: #ecf5ff;
border-radius: 4px;
font-size: 13px;
color: #409eff;
display: flex;
align-items: center;
}
</style>