【功能完善】IoT:设备新增时,增加 deviceKey、serialNumber、picUrl 等字段

pull/620/MERGE
YunaiV 2024-12-14 16:10:23 +08:00
parent 8b59786fbf
commit dde5911b5f
12 changed files with 235 additions and 119 deletions

View File

@ -91,6 +91,11 @@ export const DeviceApi = {
return await request.get({ url: `/iot/device/count?productId=` + productId })
},
// 获取设备的精简信息列表
getSimpleDeviceList: async (deviceType?: number) => {
return await request.get({ url: `/iot/device/simple-list?`, params: { deviceType } })
},
// 获取设备属性最新数据
getDevicePropertiesLatestData: async (params: any) => {
return await request.get({ url: `/iot/device/data/latest`, params })

View File

@ -13,6 +13,7 @@
placeholder="请选择产品"
:disabled="formType === 'update'"
clearable
@change="handleProductChange"
>
<el-option
v-for="product in products"
@ -22,6 +23,19 @@
/>
</el-select>
</el-form-item>
<el-form-item label="DeviceKey" prop="deviceKey">
<el-input
v-model="formData.deviceKey"
placeholder="请输入 DeviceKey"
:disabled="formType === 'update'"
>
<template #append>
<el-button @click="generateDeviceKey" :disabled="formType === 'update'">
重新生成
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="DeviceName" prop="deviceName">
<el-input
v-model="formData.deviceName"
@ -29,9 +43,30 @@
:disabled="formType === 'update'"
/>
</el-form-item>
<el-form-item label="备注名称" prop="nickname">
<el-input v-model="formData.nickname" placeholder="请输入备注名称" />
<el-form-item v-if="formData.deviceType === 1" label="网关设备" prop="gatewayId">
<el-select v-model="formData.gatewayId" placeholder="子设备可选择父设备" clearable>
<el-option
v-for="gateway in gatewayDevices"
:key="gateway.id"
:label="gateway.nickname || gateway.deviceName"
:value="gateway.id"
/>
</el-select>
</el-form-item>
<el-collapse>
<el-collapse-item title="更多配置">
<el-form-item label="备注名称" prop="nickname">
<el-input v-model="formData.nickname" placeholder="请输入备注名称" />
</el-form-item>
<el-form-item label="设备图片" prop="picUrl">
<UploadImg v-model="formData.picUrl" :height="'120px'" :width="'120px'" />
</el-form-item>
<el-form-item label="设备序列号" prop="serialNumber">
<el-input v-model="formData.serialNumber" placeholder="请输入设备序列号" />
</el-form-item>
</el-collapse-item>
</el-collapse>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
@ -41,13 +76,15 @@
</template>
<script setup lang="ts">
import { DeviceApi, DeviceVO } from '@/api/iot/device'
import { ProductApi } from '@/api/iot/product/product'
import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product'
import { UploadImg } from '@/components/UploadFile'
import { generateRandomStr } from '@/utils'
/** IoT 设备 表单 */
/** IoT 设备表单 */
defineOptions({ name: 'IoTDeviceForm' })
const { t } = useI18n() //
const message = useMessage() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
@ -56,12 +93,26 @@ const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
productId: undefined,
deviceKey: undefined as string | undefined,
deviceName: undefined,
nickname: undefined
nickname: undefined,
picUrl: undefined,
gatewayId: undefined,
deviceType: undefined as number | undefined,
serialNumber: undefined
})
const formRules = reactive({
productId: [{ required: true, message: '产品不能为空', trigger: 'blur' }],
deviceKey: [
{ required: true, message: 'DeviceKey 不能为空', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9]+$/,
message: 'DeviceKey 只能包含字母和数字',
trigger: 'blur'
}
],
deviceName: [
{ required: true, message: 'DeviceName 不能为空', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_.\-:@]{4,32}$/,
message:
@ -87,9 +138,18 @@ const formRules = reactive({
},
trigger: 'blur'
}
],
serialNumber: [
{
pattern: /^[a-zA-Z0-9-_]+$/,
message: '序列号只能包含字母、数字、中划线和下划线',
trigger: 'blur'
}
]
})
const formRef = ref() // Ref
const products = ref<ProductVO[]>([]) //
const gatewayDevices = ref<DeviceVO[]>([]) //
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
@ -97,6 +157,7 @@ const open = async (type: string, id?: number) => {
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
@ -105,7 +166,18 @@ const open = async (type: string, id?: number) => {
} finally {
formLoading.value = false
}
} else {
generateDeviceKey()
}
//
try {
gatewayDevices.value = await DeviceApi.getSimpleDeviceList(DeviceTypeEnum.GATEWAY)
} catch (error) {
console.error('加载网关设备列表失败:', error)
}
//
products.value = await ProductApi.getSimpleProductList()
}
defineExpose({ open }) // open
@ -138,19 +210,29 @@ const resetForm = () => {
formData.value = {
id: undefined,
productId: undefined,
deviceKey: undefined,
deviceName: undefined,
nickname: undefined
nickname: undefined,
picUrl: undefined,
gatewayId: undefined,
deviceType: undefined,
serialNumber: undefined
}
formRef.value?.resetFields()
}
/** 查询字典下拉列表 */
const products = ref()
const getProducts = async () => {
products.value = await ProductApi.getSimpleProductList()
/** 产品选择变化 */
const handleProductChange = (productId: number) => {
if (!productId) {
formData.value.deviceType = undefined
return
}
const product = products.value?.find((item) => item.id === productId)
formData.value.deviceType = product?.deviceType
}
onMounted(() => {
getProducts()
})
/** 生成 DeviceKey */
const generateDeviceKey = () => {
formData.value.deviceKey = generateRandomStr(16)
}
</script>

View File

@ -12,7 +12,13 @@
v-model="formData.productKey"
placeholder="请输入 ProductKey"
:readonly="formType === 'update'"
/>
>
<template #append>
<el-button @click="generateProductKey" :disabled="formType === 'update'">
重新生成
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="产品名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入产品名称" />
@ -184,7 +190,7 @@ const open = async (type: string, id?: number) => {
}
} else {
// productKey
formData.value.productKey = generateRandomStr(16)
generateProductKey()
}
//
categoryList.value = await ProductCategoryApi.getSimpleProductCategoryList()
@ -231,4 +237,9 @@ const resetForm = () => {
}
formRef.value?.resetFields()
}
/** 生成 ProductKey */
const generateProductKey = () => {
formData.value.productKey = generateRandomStr(16)
}
</script>

View File

@ -3,9 +3,9 @@
<el-tabs>
<el-tab-pane label="基础通信 Topic">
<Table
:columns="columns1"
:data="data1"
:span-method="createSpanMethod(data1)"
:columns="basicColumn"
:data="basicData"
:span-method="createSpanMethod(basicData)"
align="left"
headerAlign="left"
border="true"
@ -13,9 +13,9 @@
</el-tab-pane>
<el-tab-pane label="物模型通信 Topic">
<Table
:columns="columns2"
:data="data2"
:span-method="createSpanMethod(data2)"
:columns="functionColumn"
:data="functionData"
:span-method="createSpanMethod(functionData)"
align="left"
headerAlign="left"
border="true"
@ -29,23 +29,18 @@ import { ProductVO } from '@/api/iot/product/product'
const props = defineProps<{ product: ProductVO }>()
//
const columns1 = reactive([
// TODO
// Topic
const basicColumn = reactive([
{ label: '功能', field: 'function', width: 150 },
{ label: 'Topic 类', field: 'topicClass', width: 800 },
{ label: '操作权限', field: 'operationPermission', width: 100 },
{ label: '描述', field: 'description' }
])
const columns2 = reactive([
{ label: '功能', field: 'function', width: 150 },
{ label: 'Topic 类', field: 'topicClass', width: 800 },
{ label: '操作权限', field: 'operationPermission', width: 100 },
{ label: '描述', field: 'description' }
])
// TODO @haohao便 /Users/yunai/Java/yudao-ui-admin-vue3/src/views/ai/utils/constants.ts
const data1 = computed(() => {
// Topic
const basicData = computed(() => {
if (!props.product || !props.product.productKey) return []
return [
{
@ -147,7 +142,16 @@ const data1 = computed(() => {
]
})
const data2 = computed(() => {
// Topic
const functionColumn = reactive([
{ label: '功能', field: 'function', width: 150 },
{ label: 'Topic 类', field: 'topicClass', width: 800 },
{ label: '操作权限', field: 'operationPermission', width: 100 },
{ label: '描述', field: 'description' }
])
// Topic
const functionData = computed(() => {
if (!props.product || !props.product.productKey) return []
return [
{

View File

@ -36,7 +36,7 @@ const message = useMessage()
const id = Number(route.params.id) //
const loading = ref(true) //
const product = ref<ProductVO>({} as ProductVO) //
const activeTab = ref('info') //
const activeTab = ref('info') // info
/** 获取详情 */
const getProductData = async (id: number) => {
@ -66,6 +66,11 @@ onMounted(async () => {
return
}
await getProductData(id)
// tab
const { tab } = route.query
if (tab) {
activeTab.value = tab as string
}
//
if (product.value.id) {
product.value.deviceCount = await getDeviceCount(product.value.id)

View File

@ -63,96 +63,89 @@
<!-- 卡片视图 -->
<ContentWrap>
<div v-if="viewMode === 'card'" class="flex flex-wrap gap-4">
<el-card
v-for="item in list"
:key="item.id"
class="w-[calc(25%-12px)] transition-colors"
:body-style="{ padding: '0' }"
>
<!-- 内容区域 -->
<div class="p-4">
<!-- 标题区域 -->
<div class="flex items-center mb-3">
<div class="mr-2.5 flex items-center">
<el-image
:src="item.icon || defaultIconUrl"
class="w-[35px] h-[35px]"
fit="contain"
/>
<el-row v-if="viewMode === 'card'" :gutter="16">
<el-col v-for="item in list" :key="item.id" :xs="24" :sm="12" :md="12" :lg="6" class="mb-4">
<el-card class="h-full transition-colors" :body-style="{ padding: '0' }">
<!-- 内容区域 -->
<div class="p-4">
<!-- 标题区域 -->
<div class="flex items-center mb-3">
<div class="mr-2.5 flex items-center">
<el-image :src="item.icon || defaultIconUrl" class="w-[35px] h-[35px]" />
</div>
<div class="text-[16px] font-600">{{ item.name }}</div>
</div>
<div class="text-[16px] font-600">{{ item.name }}</div>
</div>
<!-- 信息区域 -->
<div class="flex items-center text-[14px]">
<div class="flex-1">
<div class="mb-2.5 last:mb-0">
<span class="text-[#717c8e] mr-2.5">产品分类</span>
<span class="text-[#0070ff]">{{ item.categoryName }}</span>
<!-- 信息区域 -->
<div class="flex items-center text-[14px]">
<div class="flex-1">
<div class="mb-2.5 last:mb-0">
<span class="text-[#717c8e] mr-2.5">产品分类</span>
<span class="text-[#0070ff]">{{ item.categoryName }}</span>
</div>
<div class="mb-2.5 last:mb-0">
<span class="text-[#717c8e] mr-2.5">产品类型</span>
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="item.deviceType" />
</div>
<div class="mb-2.5 last:mb-0">
<span class="text-[#717c8e] mr-2.5">产品标识</span>
<span class="text-[#0b1d30]">{{ item.productKey }}</span>
</div>
</div>
<div class="mb-2.5 last:mb-0">
<span class="text-[#717c8e] mr-2.5">产品类型</span>
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="item.deviceType" />
</div>
<div class="mb-2.5 last:mb-0">
<span class="text-[#717c8e] mr-2.5">产品标识</span>
<span class="text-[#0b1d30]">{{ item.productKey }}</span>
<div class="w-[100px] h-[100px]">
<el-image :src="item.picUrl || defaultPicUrl" class="w-full h-full" />
</div>
</div>
<div class="w-[100px] h-[100px]">
<el-image :src="item.picUrl || defaultPicUrl" class="w-full h-full" fit="cover" />
<!-- 分隔线 -->
<el-divider class="!my-3" />
<!-- 按钮组 -->
<div class="flex items-center px-0">
<el-button
class="flex-1 !px-2 !h-[32px] text-[13px]"
type="primary"
plain
@click="openForm('update', item.id)"
v-hasPermi="['iot:product:update']"
>
<Icon icon="ep:edit-pen" class="mr-1" />
编辑
</el-button>
<el-button
class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
type="warning"
plain
@click="openDetail(item.id)"
>
<Icon icon="ep:view" class="mr-1" />
详情
</el-button>
<el-button
class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
type="success"
plain
@click="openObjectModel(item)"
>
<Icon icon="ep:scale-to-original" class="mr-1" />
物模型
</el-button>
<div class="mx-[10px] h-[20px] w-[1px] bg-[#dcdfe6]"></div>
<el-button
class="!px-2 !h-[32px] text-[13px]"
type="danger"
plain
@click="handleDelete(item.id)"
v-hasPermi="['iot:product:delete']"
:disabled="item.status === 1"
>
<Icon icon="ep:delete" />
</el-button>
</div>
</div>
<!-- 分隔线 -->
<el-divider class="!my-3" />
<!-- 按钮组 -->
<div class="flex items-center">
<el-button
class="flex-1 !px-2 !h-[32px]"
type="primary"
plain
@click="openForm('update', item.id)"
v-hasPermi="['iot:product:update']"
>
<Icon icon="ep:edit-pen" class="mr-1" />
编辑
</el-button>
<el-button
class="flex-1 !px-2 !h-[32px] !ml-[12px]"
type="warning"
plain
@click="openDetail(item.id)"
>
<Icon icon="ep:view" class="mr-1" />
详情
</el-button>
<el-button
class="flex-1 !px-2 !h-[32px] !ml-[12px]"
type="success"
plain
@click="openObjectModel(item)"
>
<Icon icon="ep:scale-to-original" class="mr-1" />
物模型
</el-button>
<div class="mx-[12px] h-[20px] w-[1px] bg-[#dcdfe6]"></div>
<el-button
class="!px-2 !h-[32px]"
type="danger"
plain
@click="handleDelete(item.id)"
v-hasPermi="['iot:product:delete']"
:disabled="item.status === 1"
>
<Icon icon="ep:delete" />
</el-button>
</div>
</div>
</el-card>
</div>
</el-card>
</el-col>
</el-row>
<!-- 列表视图 -->
<el-table v-else v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
@ -251,8 +244,11 @@ defineOptions({ name: 'IoTProduct' })
const message = useMessage() //
const { t } = useI18n() //
const { push } = useRouter()
const route = useRoute()
const loading = ref(true) //
const activeName = ref('info') //
const list = ref<ProductVO[]>([]) //
const total = ref(0) //
const queryParams = reactive({
@ -296,11 +292,19 @@ const openForm = (type: string, id?: number) => {
}
/** 打开详情 */
const { push } = useRouter()
const openDetail = (id: number) => {
push({ name: 'IoTProductDetail', params: { id } })
}
/** 打开物模型 */
const openObjectModel = (item: ProductVO) => {
push({
name: 'IoTProductDetail',
params: { id: item.id },
query: { tab: 'function' }
})
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
@ -332,5 +336,10 @@ const handleExport = async () => {
/** 初始化 **/
onMounted(() => {
getList()
// tab
const { tab } = route.query
if (tab) {
activeName.value = tab as string
}
})
</script>