【功能完善】IoT:设备新增时,增加 deviceKey、serialNumber、picUrl 等字段
parent
8b59786fbf
commit
dde5911b5f
|
@ -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 })
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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 [
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue