feat(iot):【网关设备:20%】增加网关设备绑定能力(未完成),基于 breezy-doodling-starlight.md 规划

This commit is contained in:
YunaiV
2026-01-22 00:51:48 +08:00
parent d76481dcb4
commit 2515caed35
3 changed files with 244 additions and 1 deletions

View File

@@ -158,5 +158,31 @@ export const DeviceApi = {
// 发送设备消息
sendDeviceMessage: async (params: IotDeviceMessageSendReqVO) => {
return await request.post({ url: `/iot/device/message/send`, data: params })
},
// 绑定子设备到网关
bindDeviceGateway: async (data: { ids: number[]; gatewayId: number }) => {
return await request.put({ url: `/iot/device/bind-gateway`, data })
},
// 解绑子设备与网关
unbindDeviceGateway: async (data: { ids: number[] }) => {
return await request.put({ url: `/iot/device/unbind-gateway`, data })
},
// 获取网关的子设备列表
getSubDeviceList: async (gatewayId: number) => {
return await request.get<DeviceVO[]>({
url: `/iot/device/sub-device-list`,
params: { gatewayId }
})
},
// 获取可绑定到网关的子设备列表
getBindableSubDeviceList: async (gatewayId?: number) => {
return await request.get<DeviceVO[]>({
url: `/iot/device/bindable-sub-device-list`,
params: { gatewayId }
})
}
}

View File

@@ -0,0 +1,210 @@
<!-- 子设备管理 -->
<template>
<ContentWrap>
<!-- 操作按钮 -->
<div class="mb-4">
<el-button type="primary" plain @click="openBindDialog" v-hasPermi="['iot:device:update']">
<Icon icon="ep:plus" class="mr-5px" /> 添加子设备
</el-button>
<el-button
type="danger"
plain
@click="handleUnbindBatch"
:disabled="selectedIds.length === 0"
v-hasPermi="['iot:device:update']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量解绑
</el-button>
</div>
<!-- 子设备列表 -->
<el-table
v-loading="loading"
:data="subDeviceList"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="DeviceName" align="center" prop="deviceName">
<template #default="{ row }">
<el-link type="primary" @click="openDeviceDetail(row.id)">{{ row.deviceName }}</el-link>
</template>
</el-table-column>
<el-table-column label="备注名称" align="center" prop="nickname" />
<el-table-column label="产品名称" align="center" prop="productName" />
<el-table-column label="设备状态" align="center" prop="state">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="row.state" />
</template>
</el-table-column>
<el-table-column
label="最后上线时间"
align="center"
prop="onlineTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" width="120px">
<template #default="{ row }">
<el-button link type="primary" @click="openDeviceDetail(row.id)"> 查看 </el-button>
<el-button
link
type="danger"
@click="handleUnbind(row.id)"
v-hasPermi="['iot:device:update']"
>
解绑
</el-button>
</template>
</el-table-column>
</el-table>
</ContentWrap>
<!-- 添加子设备弹窗 -->
<!-- TODO @AI需要增加检索产品设备等检索可以一起讨论下 -->
<Dialog title="添加子设备" v-model="bindDialogVisible" width="800px">
<el-table
ref="bindTableRef"
v-loading="bindFormLoading"
:data="bindableDevices"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleBindSelectionChange"
max-height="400px"
>
<el-table-column type="selection" width="55" />
<el-table-column label="DeviceName" align="center" prop="deviceName" />
<el-table-column label="备注名称" align="center" prop="nickname" />
<el-table-column label="产品名称" align="center" prop="productName" />
<el-table-column label="设备状态" align="center" prop="state">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="row.state" />
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="bindDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleBindSubmit" :loading="bindFormLoading">
确定已选 {{ bindSelectedIds.length }}
</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
const props = defineProps<{
gatewayId: number
}>()
const message = useMessage()
const { push } = useRouter()
// TODO @AI注释使用尾注释
// 列表数据
const loading = ref(false)
const subDeviceList = ref<DeviceVO[]>([])
const selectedIds = ref<number[]>([])
// 绑定弹窗数据
const bindDialogVisible = ref(false)
const bindFormLoading = ref(false)
const bindTableRef = ref()
const bindableDevices = ref<DeviceVO[]>([])
const bindSelectedIds = ref<number[]>([])
/** 获取子设备列表 */
const getSubDeviceList = async () => {
loading.value = true
try {
subDeviceList.value = await DeviceApi.getSubDeviceList(props.gatewayId)
} finally {
loading.value = false
}
}
/** 打开设备详情 */
const openDeviceDetail = (id: number) => {
push({ name: 'IoTDeviceDetail', params: { id } })
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: DeviceVO[]) => {
selectedIds.value = selection.map((item) => item.id)
}
/** 打开绑定弹窗 */
const openBindDialog = async () => {
bindSelectedIds.value = []
bindDialogVisible.value = true
bindFormLoading.value = true
try {
// 获取可绑定的子设备列表
const list = await DeviceApi.getBindableSubDeviceList(props.gatewayId)
// 排除已绑定到当前网关的设备
// TODO @AI不用排除后端已经排除
bindableDevices.value = list.filter((device: DeviceVO) => device.gatewayId !== props.gatewayId)
} finally {
bindFormLoading.value = false
}
}
/** 绑定弹窗多选框选中数据 */
const handleBindSelectionChange = (selection: DeviceVO[]) => {
bindSelectedIds.value = selection.map((item) => item.id)
}
/** 提交绑定 */
const handleBindSubmit = async () => {
if (bindSelectedIds.value.length === 0) {
message.warning('请选择要绑定的子设备')
return
}
bindFormLoading.value = true
try {
await DeviceApi.bindDeviceGateway({
ids: bindSelectedIds.value,
gatewayId: props.gatewayId
})
message.success('绑定成功')
bindDialogVisible.value = false
await getSubDeviceList()
} finally {
bindFormLoading.value = false
}
}
/** 解绑单个设备 */
const handleUnbind = async (id: number) => {
try {
await message.confirm('确定要解绑该子设备吗?')
await DeviceApi.unbindDeviceGateway({ ids: [id] })
message.success('解绑成功')
await getSubDeviceList()
} catch {}
}
/** 批量解绑 */
const handleUnbindBatch = async () => {
try {
await message.confirm(`确定要解绑选中的 ${selectedIds.value.length} 个子设备吗?`)
await DeviceApi.unbindDeviceGateway({ ids: selectedIds.value })
message.success('批量解绑成功')
selectedIds.value = []
await getSubDeviceList()
} catch {}
}
/** 初始化 */
onMounted(async () => {
await getSubDeviceList()
})
// 暴露刷新方法
// TODO @AIrefresh 需要提供么?
defineExpose({ refresh: getSubDeviceList })
</script>

View File

@@ -17,7 +17,13 @@
:thing-model-list="thingModelList"
/>
</el-tab-pane>
<el-tab-pane label="子设备管理" v-if="product.deviceType === DeviceTypeEnum.GATEWAY" />
<el-tab-pane
label="子设备管理"
name="subDevice"
v-if="product.deviceType === DeviceTypeEnum.GATEWAY"
>
<DeviceDetailsSubDevice v-if="activeTab === 'subDevice'" :gateway-id="device.id" />
</el-tab-pane>
<el-tab-pane label="设备消息" name="log">
<DeviceDetailsMessage v-if="activeTab === 'log'" :device-id="device.id" />
</el-tab-pane>
@@ -50,6 +56,7 @@ import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue'
import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue'
import DeviceDetailConfig from './DeviceDetailConfig.vue'
import DeviceDetailsSubDevice from './DeviceDetailsSubDevice.vue'
defineOptions({ name: 'IoTDeviceDetail' })