Browse Source

feat: 设备详情 - 最新上报数据 / 已订阅 Topic

main
刘凯 1 year ago
parent
commit
a80242c53c
  1. 31
      src/api/device-manage/device/index.ts
  2. 8
      src/api/device-manage/device/types.ts
  3. 103
      src/views/device-manage/device/components/DeviceInfo.vue
  4. 53
      src/views/device-manage/device/components/TopicList.vue
  5. 90
      src/views/device-manage/device/components/composables/useDeviceInfo.ts
  6. 27
      src/views/device-manage/device/components/composables/useDeviceProperties.ts
  7. 2
      src/views/device-manage/device/components/index.ts
  8. 29
      src/views/device-manage/device/detail.vue
  9. 19
      src/views/product/components/composables/useModelService.ts

31
src/api/device-manage/device/index.ts

@ -1,4 +1,4 @@
import type { Device, GetDeviceListParams } from './types'
import type { Device, DevicePropertie, GetDeviceListParams } from './types'
import { defHttp } from '@/utils/http/axios'
export function getDeviceList(params: GetDeviceListParams) {
@ -27,3 +27,32 @@ export function deleteDevice(id: string) {
url: `/device/remove?id=${id}`,
})
}
export function getDeviceDetail(id: string) {
return defHttp.get<Device>({
url: '/device/detail',
params: {
id,
},
})
}
export function getDeviceProperties(modelId: string, deviceSn: string) {
return defHttp.get<{
properties?: DevicePropertie[]
updateTime?: string
}>({
url: '/device/properties',
params: {
deviceSn,
modelId,
},
})
}
export function getDeviceTopicList(params: PageParam & { deviceId: string }) {
return defHttp.get({
url: '/device/topic/page',
params,
})
}

8
src/api/device-manage/device/types.ts

@ -13,3 +13,11 @@ export interface Device {
deviceDesc: string
isOnline: BooleanFlag
}
export interface DevicePropertie {
identifier: string
name: string
unit: string
value: string
sort: number
}

103
src/views/device-manage/device/components/DeviceInfo.vue

@ -0,0 +1,103 @@
<script lang="ts" setup>
import { Card, Empty } from 'ant-design-vue'
import { FieldTimeOutlined, SyncOutlined } from '@ant-design/icons-vue'
import { useDeviceInfo } from './composables/useDeviceInfo'
import { useDeviceProperties } from './composables/useDeviceProperties'
import { Description } from '@/components/Description'
import { useModelService } from '@/views/product/components/composables/useModelService'
const { data, scheam } = useDeviceInfo()
const {
modelServiceList,
selectedModelId,
setSelectedModelId,
} = useModelService(() => data.value?.productId)
const {
isLoading,
deviceProperties,
reloadReviceProperties,
} = useDeviceProperties(() => selectedModelId.value, () => data.value?.deviceSn)
</script>
<template>
<div>
<Description :schema="scheam" :data="data" :column="2" :label-style="{ width: '130px' }" />
<Card mt="15px">
<div flex="~ items-center justify-between">
<div font-bold>
最新上报数据
</div>
<div flex="~ items-center">
<span v-if="deviceProperties" text="gray-400 center" mr="12px">
更新时间: {{ deviceProperties.updateTime }}
</span>
<a-button size="small" @click="reloadReviceProperties">
<SyncOutlined />
刷新
</a-button>
</div>
</div>
<div v-loading="isLoading" flex="~ gap-12px" mt="12px">
<div w="20%" border="0 r-1 solid gray-50">
<div
v-for="item in modelServiceList" :key="item.id"
flex="~ items-center justify-between"
class="box-border h-60px cursor-pointer pl-10px hover:bg-gray-100"
border="0 b-1 solid gray-50"
:class="selectedModelId === item.id ? 'bg-gray-100' : ''"
@click="setSelectedModelId(item.id)"
>
<div>
<div truncate>
{{ item.serviceId }}
</div>
<div mt="5px" text="12px gray-500" truncate :title="item.description">
{{ item.description }}
</div>
</div>
</div>
</div>
<div
v-if="deviceProperties && deviceProperties.properties"
w-0 flex-1
grid="~ cols-4 rows-[160px] auto-rows-[160px] gap-12px"
>
<div
v-for="item in deviceProperties.properties"
:key="item.identifier"
p="15px" box-border rounded
shadow="hover:lg" transition="shadow"
flex="~ col justify-between"
style="background: linear-gradient(to right, rgba(243, 244, 246, 1), rgba(209, 216, 224, 1));"
>
<div>
<div font-500 text="16px">
{{ item.name }}
</div>
<div text="gray-500">
{{ item.identifier }}
</div>
</div>
<div text="22px center">
"{{ item.unit || 'null' }}"
</div>
<div text="right gray-500 hover:gray-800">
<span class="cursor-pointer">
<FieldTimeOutlined />
历史
</span>
</div>
</div>
</div>
<div v-else text="center" flex-1>
<Empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</div>
</div>
</Card>
</div>
</template>

53
src/views/device-manage/device/components/TopicList.vue

@ -0,0 +1,53 @@
<script lang="ts" setup>
import { h } from 'vue'
import { Alert, Tag } from 'ant-design-vue'
import { SyncOutlined } from '@ant-design/icons-vue'
import { useRoute } from 'vue-router'
import { BasicTable, useTable } from '@/components/Table'
import { getDeviceTopicList } from '@/api/device-manage/device'
import { useSystemEnumStore } from '@/store/modules/systemEnum'
const route = useRoute()
const { getSystemEnumLabel } = useSystemEnumStore()
const [register, { reload }] = useTable({
api(params) {
return getDeviceTopicList({ ...params, deviceId: route.params.id as string })
},
columns: [
{
title: 'Topic',
dataIndex: 'topic',
},
{
title: '操作权限',
dataIndex: 'topicType',
customRender({ value }) {
return h(Tag, () => getSystemEnumLabel('eProductTopicType', value))
},
},
{
title: '描述',
dataIndex: 'topicDesc',
},
],
bordered: true,
inset: true,
canResize: false,
})
</script>
<template>
<div flex="~ items-center justify-between">
<Alert
type="info"
show-icon
class="py-4px text-13px"
message="Topic用以将设备数据分类上报,进而分别进行处理。除了系统预置的Topic,您也可以自定义Topic,然后在设备侧开发时选择数据上报的Topic。"
/>
<a-button size="small" @click="reload">
<SyncOutlined />
刷新
</a-button>
</div>
<BasicTable mt="12px" @register="register" />
</template>

90
src/views/device-manage/device/components/composables/useDeviceInfo.ts

@ -0,0 +1,90 @@
import { h } from 'vue'
import { Button, Tag } from 'ant-design-vue'
import { EyeOutlined } from '@ant-design/icons-vue'
import { useRoute } from 'vue-router'
import { useAsyncState } from '@vueuse/core'
import type { DescItem } from '@/components/Description'
import { getDeviceDetail } from '@/api/device-manage/device'
export function useDeviceInfo() {
const scheam: DescItem[] = [
{
field: 'productName',
label: '所属产品',
},
{
field: 'deviceName',
label: '设备名称',
},
{
field: 'deviceSn',
label: '设备序列号',
},
{
field: 'deviceSecret',
label: '设备密钥',
},
{
field: 'isActive',
label: '设备状态',
render(value) {
return h(Tag, { color: value ? 'green' : 'default' }, () => value ? '已激活' : '未激活')
},
},
{
field: 'isOnline',
label: '在线状态',
render(value) {
return h(Tag, { color: value ? 'green' : 'default' }, () => value ? '在线' : '离线')
},
},
{
field: 'createTime',
label: '创建时间',
},
{
field: 'activeTime',
label: '激活时间',
},
{
field: 'lastOnlineTime',
label: '最后上线时间',
},
{
field: 'lastOfflineTime',
label: '最后离线时间',
},
{
field: 'mqtt',
label: 'MQTT连接参数',
render: () => h(Button, {
size: 'small',
onClick: openMqttParamsModal,
}, () => [h(EyeOutlined), '查看参数']),
},
{
field: 'report',
label: '上报示例',
render: () => h(Button, {
size: 'small',
onClick: openReportExampleModal,
}, () => [h(EyeOutlined), '查看示例']),
},
{
field: 'deviceDesc',
label: '设备描述',
},
]
const route = useRoute()
const { state: data } = useAsyncState(() => getDeviceDetail(route.params.id as string), undefined)
function openMqttParamsModal() {}
function openReportExampleModal() {}
return {
data,
scheam,
}
}

27
src/views/device-manage/device/components/composables/useDeviceProperties.ts

@ -0,0 +1,27 @@
import { useAsyncState } from '@vueuse/core'
import { type MaybeRefOrGetter, toValue, watchEffect } from 'vue'
import { getDeviceProperties } from '@/api/device-manage/device'
export function useDeviceProperties(modelId: MaybeRefOrGetter<string | undefined>, deviceSn: MaybeRefOrGetter<string | undefined>) {
const { state, execute, isLoading } = useAsyncState(
() => getDeviceProperties(toValue(modelId)!, toValue(deviceSn)!),
undefined,
{
immediate: false,
resetOnExecute: false,
},
)
watchEffect(() => {
if (!toValue(modelId) || !toValue(deviceSn))
return
execute()
})
return {
isLoading,
deviceProperties: state,
reloadReviceProperties: execute,
}
}

2
src/views/device-manage/device/components/index.ts

@ -0,0 +1,2 @@
export { default as DeviceInfo } from './DeviceInfo.vue'
export { default as TopicList } from './TopicList.vue'

29
src/views/device-manage/device/detail.vue

@ -1,5 +1,30 @@
<script lang='ts' setup>
import { Card, Tabs } from 'ant-design-vue'
import { DeviceInfo, TopicList } from './components'
defineOptions({ name: 'DeviceDetail' })
</script>
<template>
<div>
Detail
<div p="12px">
<Card title="设备详情">
<Tabs>
<Tabs.TabPane key="1" tab="设备信息">
<DeviceInfo />
</Tabs.TabPane>
<Tabs.TabPane key="2" tab="已订阅 Topic">
<TopicList />
</Tabs.TabPane>
<Tabs.TabPane key="3" tab="云端下发">
TODO
</Tabs.TabPane>
</Tabs>
</Card>
</div>
</template>
<style scoped lang="less">
:deep(.ant-card-body) {
padding-top: 10px;
}
</style>

19
src/views/product/components/composables/useModelService.ts

@ -1,15 +1,24 @@
import { useAsyncState, watchOnce } from '@vueuse/core'
import type { MaybeRef } from 'vue'
import { ref, unref } from 'vue'
import { toValue, useAsyncState, watchOnce } from '@vueuse/core'
import type { MaybeRefOrGetter } from 'vue'
import { ref, watch } from 'vue'
import { deleteModelService, getAllModelServices } from '@/api/product/model'
import { useMessage } from '@/hooks/web/useMessage'
import { useModal } from '@/components/Modal'
import type { ModelService } from '@/api/product/types'
export function useModelService(productId: MaybeRef<string>) {
export function useModelService(productId: MaybeRefOrGetter<string | undefined>) {
const selectedModelId = ref<string>()
const { state, execute } = useAsyncState(() => getAllModelServices(unref(productId)), [], { resetOnExecute: false })
const { state, execute } = useAsyncState(
() => getAllModelServices(toValue(productId) as string),
[],
{
resetOnExecute: false,
immediate: toValue(productId) !== undefined,
},
)
watch(() => toValue(productId), () => execute())
watchOnce(state, () => {
if (state.value.length > 0)

Loading…
Cancel
Save