9 changed files with 354 additions and 8 deletions
@ -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> |
@ -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> |
@ -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, |
||||||
|
} |
||||||
|
} |
@ -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, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,2 @@ |
|||||||
|
export { default as DeviceInfo } from './DeviceInfo.vue' |
||||||
|
export { default as TopicList } from './TopicList.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> |
<template> |
||||||
<div> |
<div p="12px"> |
||||||
Detail |
<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> |
</div> |
||||||
</template> |
</template> |
||||||
|
|
||||||
|
<style scoped lang="less"> |
||||||
|
:deep(.ant-card-body) { |
||||||
|
padding-top: 10px; |
||||||
|
} |
||||||
|
</style> |
||||||
|
Loading…
Reference in new issue