fastbee/yack-web/src/views/iot/device/sub-device-create.vue
2026-03-09 23:18:35 +08:00

356 lines
10 KiB
Vue

<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>