Commit 39ed06d6535485f9e77e8dcd85bd9983590bd4f0
1 parent
7b757491
feat: device list page drawer add history data panel
Showing
7 changed files
with
251 additions
and
15 deletions
src/hooks/component/useGridLayout.ts
0 → 100644
| 1 | +import type { ColProps } from 'ant-design-vue'; | ||
| 2 | + | ||
| 3 | +export function useGridLayout(all: number): ColProps; | ||
| 4 | +export function useGridLayout(xs: number, lg: number, xxl: number): ColProps; | ||
| 5 | +export function useGridLayout( | ||
| 6 | + xs: number, | ||
| 7 | + sm: number, | ||
| 8 | + md: number, | ||
| 9 | + lg: number, | ||
| 10 | + xl: number, | ||
| 11 | + xxl: number | ||
| 12 | +): ColProps; | ||
| 13 | +export function useGridLayout( | ||
| 14 | + xs: number, | ||
| 15 | + sm?: number, | ||
| 16 | + md?: number, | ||
| 17 | + lg?: number, | ||
| 18 | + xl?: number, | ||
| 19 | + xxl?: number | ||
| 20 | +): ColProps { | ||
| 21 | + const length = arguments.length; | ||
| 22 | + const maxCol = 24; | ||
| 23 | + | ||
| 24 | + const toCol = (number: number) => { | ||
| 25 | + return Math.floor(maxCol / number); | ||
| 26 | + }; | ||
| 27 | + | ||
| 28 | + switch (length) { | ||
| 29 | + case 1: | ||
| 30 | + return { span: toCol(xs) }; | ||
| 31 | + case 3: | ||
| 32 | + return { xs: toCol(xs), lg: toCol(sm!), xxl: toCol(md!) }; | ||
| 33 | + case 6: | ||
| 34 | + return { | ||
| 35 | + xs: toCol(xs), | ||
| 36 | + sm: toCol(sm!), | ||
| 37 | + md: toCol(md!), | ||
| 38 | + lg: toCol(lg!), | ||
| 39 | + xl: toCol(xl!), | ||
| 40 | + xxl: toCol(xxl!), | ||
| 41 | + }; | ||
| 42 | + default: | ||
| 43 | + return { span: toCol(xs) ?? 6 }; | ||
| 44 | + } | ||
| 45 | +} |
| @@ -46,7 +46,8 @@ export const AccountDetail: AppRouteRecordRaw = { | @@ -46,7 +46,8 @@ export const AccountDetail: AppRouteRecordRaw = { | ||
| 46 | { | 46 | { |
| 47 | path: 'account_detail/:id', | 47 | path: 'account_detail/:id', |
| 48 | name: 'Account_Detail', | 48 | name: 'Account_Detail', |
| 49 | - component: () => import('/@/views/system/account/AccountDetail.vue'), | 49 | + // component: () => import('/@/views/system/account/AccountDetail.vue'), |
| 50 | + component: () => import('../../views/alarm/config/ContactDrawer.vue'), | ||
| 50 | meta: { | 51 | meta: { |
| 51 | title: '用户详情', | 52 | title: '用户详情', |
| 52 | }, | 53 | }, |
| @@ -15,6 +15,9 @@ | @@ -15,6 +15,9 @@ | ||
| 15 | <TabPane key="2" tab="实时数据" v-if="deviceDetail?.deviceType !== 'GATEWAY'"> | 15 | <TabPane key="2" tab="实时数据" v-if="deviceDetail?.deviceType !== 'GATEWAY'"> |
| 16 | <RealTimeData :deviceDetail="deviceDetail" /> | 16 | <RealTimeData :deviceDetail="deviceDetail" /> |
| 17 | </TabPane> | 17 | </TabPane> |
| 18 | + <TabPane key="7" tab="历史数据" v-if="deviceDetail?.deviceType !== 'GATEWAY'"> | ||
| 19 | + <HistoryData :deviceDetail="deviceDetail" /> | ||
| 20 | + </TabPane> | ||
| 18 | <TabPane key="5" tab="命令下发" v-if="deviceDetail?.deviceType !== 'SENSOR'"> | 21 | <TabPane key="5" tab="命令下发" v-if="deviceDetail?.deviceType !== 'SENSOR'"> |
| 19 | <CommandIssuance :deviceDetail="deviceDetail" /> | 22 | <CommandIssuance :deviceDetail="deviceDetail" /> |
| 20 | </TabPane> | 23 | </TabPane> |
| @@ -46,6 +49,7 @@ | @@ -46,6 +49,7 @@ | ||
| 46 | import TBoxDetail from '../tabs/TBoxDetail.vue'; | 49 | import TBoxDetail from '../tabs/TBoxDetail.vue'; |
| 47 | import CommandIssuance from '../tabs/CommandIssuance.vue'; | 50 | import CommandIssuance from '../tabs/CommandIssuance.vue'; |
| 48 | import { getDeviceDetail } from '/@/api/device/deviceManager'; | 51 | import { getDeviceDetail } from '/@/api/device/deviceManager'; |
| 52 | + import HistoryData from '../tabs/HistoryData.vue'; | ||
| 49 | export default defineComponent({ | 53 | export default defineComponent({ |
| 50 | name: 'DeviceModal', | 54 | name: 'DeviceModal', |
| 51 | components: { | 55 | components: { |
| @@ -58,6 +62,7 @@ | @@ -58,6 +62,7 @@ | ||
| 58 | ChildDevice, | 62 | ChildDevice, |
| 59 | CommandIssuance, | 63 | CommandIssuance, |
| 60 | TBoxDetail, | 64 | TBoxDetail, |
| 65 | + HistoryData, | ||
| 61 | }, | 66 | }, |
| 62 | emits: ['reload', 'register'], | 67 | emits: ['reload', 'register'], |
| 63 | setup() { | 68 | setup() { |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import moment from 'moment'; | ||
| 3 | + import { nextTick, onMounted, onUnmounted, Ref, ref, unref } from 'vue'; | ||
| 4 | + import { getDeviceDataKeys, getDeviceHistoryInfo } from '/@/api/alarm/position'; | ||
| 5 | + import { Empty } from 'ant-design-vue'; | ||
| 6 | + import { useECharts } from '/@/hooks/web/useECharts'; | ||
| 7 | + import { dateUtil } from '/@/utils/dateUtil'; | ||
| 8 | + import { | ||
| 9 | + AggregateDataEnum, | ||
| 10 | + eChartOptions, | ||
| 11 | + selectDeviceAttrSchema, | ||
| 12 | + } from '/@/views/device/localtion/config.data'; | ||
| 13 | + import { useTimePeriodForm } from '/@/views/device/localtion/cpns/TimePeriodForm'; | ||
| 14 | + import { defaultSchemas, QueryWay } from '/@/views/device/localtion/cpns/TimePeriodForm/config'; | ||
| 15 | + import TimePeriodForm from '/@/views/device/localtion/cpns/TimePeriodForm/TimePeriodForm.vue'; | ||
| 16 | + import { SchemaFiled } from '/@/views/report/config/config.data'; | ||
| 17 | + import { useGridLayout } from '/@/hooks/component/useGridLayout'; | ||
| 18 | + import { ColEx } from '/@/components/Form/src/types'; | ||
| 19 | + | ||
| 20 | + interface DeviceDetail { | ||
| 21 | + tbDeviceId: string; | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + const props = defineProps<{ | ||
| 25 | + deviceDetail: DeviceDetail; | ||
| 26 | + }>(); | ||
| 27 | + | ||
| 28 | + const chartRef = ref(); | ||
| 29 | + | ||
| 30 | + const deviceAttrs = ref<string[]>([]); | ||
| 31 | + | ||
| 32 | + const loading = ref(false); | ||
| 33 | + | ||
| 34 | + const isNull = ref(false); | ||
| 35 | + | ||
| 36 | + function getSearchParams(value: Recordable) { | ||
| 37 | + const { startTs, endTs, interval, agg, limit, keys, way } = value; | ||
| 38 | + if (way === QueryWay.LATEST) { | ||
| 39 | + return { | ||
| 40 | + entityId: props.deviceDetail.tbDeviceId, | ||
| 41 | + keys: keys ? keys : unref(deviceAttrs).join(), | ||
| 42 | + startTs: moment().subtract(startTs, 'ms').valueOf(), | ||
| 43 | + endTs: Date.now(), | ||
| 44 | + interval, | ||
| 45 | + agg, | ||
| 46 | + limit, | ||
| 47 | + }; | ||
| 48 | + } else { | ||
| 49 | + return { | ||
| 50 | + entityId: props.deviceDetail.tbDeviceId, | ||
| 51 | + keys: keys ? keys : unref(deviceAttrs).join(), | ||
| 52 | + startTs: moment(startTs).valueOf(), | ||
| 53 | + endTs: moment(endTs).valueOf(), | ||
| 54 | + interval, | ||
| 55 | + agg, | ||
| 56 | + limit, | ||
| 57 | + }; | ||
| 58 | + } | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + function hasDeviceAttr() { | ||
| 62 | + if (!unref(deviceAttrs).length) { | ||
| 63 | + return false; | ||
| 64 | + } else { | ||
| 65 | + return true; | ||
| 66 | + } | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + const { setOptions, getInstance } = useECharts(chartRef as Ref<HTMLDivElement>); | ||
| 70 | + | ||
| 71 | + function setChartOptions(data, keys?) { | ||
| 72 | + const dataArray: any[] = []; | ||
| 73 | + for (const key in data) { | ||
| 74 | + for (const item1 of data[key]) { | ||
| 75 | + let { ts, value } = item1; | ||
| 76 | + const time = dateUtil(ts).format('YYYY-MM-DD HH:mm:ss'); | ||
| 77 | + value = Number(value).toFixed(2); | ||
| 78 | + dataArray.push([time, value, key]); | ||
| 79 | + } | ||
| 80 | + } | ||
| 81 | + keys = keys ? [keys] : unref(deviceAttrs); | ||
| 82 | + const series: any = keys.map((item) => { | ||
| 83 | + return { | ||
| 84 | + name: item, | ||
| 85 | + type: 'line', | ||
| 86 | + data: dataArray.filter((item1) => item1[2] === item), | ||
| 87 | + }; | ||
| 88 | + }); | ||
| 89 | + // 设置数据 | ||
| 90 | + setOptions(eChartOptions(series, keys)); | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + const [register, method] = useTimePeriodForm({ | ||
| 94 | + schemas: [...defaultSchemas, ...selectDeviceAttrSchema], | ||
| 95 | + baseColProps: useGridLayout(2, 3, 4) as unknown as ColEx, | ||
| 96 | + async submitFunc() { | ||
| 97 | + // 表单验证 | ||
| 98 | + await method.validate(); | ||
| 99 | + const value = method.getFieldsValue(); | ||
| 100 | + const searchParams = getSearchParams(value); | ||
| 101 | + | ||
| 102 | + if (!hasDeviceAttr()) return; | ||
| 103 | + // 发送请求 | ||
| 104 | + loading.value = true; | ||
| 105 | + const res = await getDeviceHistoryInfo(searchParams); | ||
| 106 | + // 判断数据对象是否为空 | ||
| 107 | + if (!Object.keys(res).length) { | ||
| 108 | + isNull.value = false; | ||
| 109 | + return; | ||
| 110 | + } else { | ||
| 111 | + isNull.value = true; | ||
| 112 | + } | ||
| 113 | + setChartOptions(res, value.keys); | ||
| 114 | + loading.value = false; | ||
| 115 | + }, | ||
| 116 | + }); | ||
| 117 | + | ||
| 118 | + const getDeviceDataKey = async () => { | ||
| 119 | + const { tbDeviceId } = props.deviceDetail || {}; | ||
| 120 | + try { | ||
| 121 | + deviceAttrs.value = (await getDeviceDataKeys(tbDeviceId)) || []; | ||
| 122 | + } catch (error) {} | ||
| 123 | + }; | ||
| 124 | + | ||
| 125 | + const openHistoryPanel = async () => { | ||
| 126 | + await nextTick(); | ||
| 127 | + method.updateSchema({ | ||
| 128 | + field: 'keys', | ||
| 129 | + componentProps: { | ||
| 130 | + options: unref(deviceAttrs).map((item) => ({ label: item, value: item })), | ||
| 131 | + }, | ||
| 132 | + }); | ||
| 133 | + | ||
| 134 | + method.setFieldsValue({ | ||
| 135 | + [SchemaFiled.START_TS]: 1 * 24 * 60 * 60 * 1000, | ||
| 136 | + [SchemaFiled.LIMIT]: 7, | ||
| 137 | + [SchemaFiled.AGG]: AggregateDataEnum.NONE, | ||
| 138 | + }); | ||
| 139 | + | ||
| 140 | + if (!hasDeviceAttr()) return; | ||
| 141 | + | ||
| 142 | + const res = await getDeviceHistoryInfo({ | ||
| 143 | + entityId: props.deviceDetail.tbDeviceId, | ||
| 144 | + keys: unref(deviceAttrs).join(), | ||
| 145 | + startTs: Date.now() - 1 * 24 * 60 * 60 * 1000, | ||
| 146 | + endTs: Date.now(), | ||
| 147 | + agg: AggregateDataEnum.NONE, | ||
| 148 | + }); | ||
| 149 | + | ||
| 150 | + // 判断对象是否为空 | ||
| 151 | + if (!Object.keys(res).length) { | ||
| 152 | + isNull.value = false; | ||
| 153 | + return; | ||
| 154 | + } else { | ||
| 155 | + isNull.value = true; | ||
| 156 | + } | ||
| 157 | + setChartOptions(res); | ||
| 158 | + }; | ||
| 159 | + | ||
| 160 | + onMounted(async () => { | ||
| 161 | + await getDeviceDataKey(); | ||
| 162 | + await openHistoryPanel(); | ||
| 163 | + }); | ||
| 164 | + | ||
| 165 | + onUnmounted(() => { | ||
| 166 | + getInstance()?.clear(); | ||
| 167 | + }); | ||
| 168 | +</script> | ||
| 169 | + | ||
| 170 | +<template> | ||
| 171 | + <section class="flex flex-col p-4 h-full" style="color: #f0f2f5"> | ||
| 172 | + <section class="bg-white p-3"> | ||
| 173 | + <TimePeriodForm @register="register" /> | ||
| 174 | + </section> | ||
| 175 | + <section class="bg-white p-3"> | ||
| 176 | + <div v-show="isNull" ref="chartRef" :style="{ height: '550px', width: '100%' }"> | ||
| 177 | + <Loading :loading="loading" :absolute="true" /> | ||
| 178 | + </div> | ||
| 179 | + <Empty v-show="!isNull" /> | ||
| 180 | + </section> | ||
| 181 | + </section> | ||
| 182 | +</template> | ||
| 183 | + | ||
| 184 | +<style scoped></style> |
| @@ -473,16 +473,9 @@ export const selectDeviceAttrSchema: FormSchema[] = [ | @@ -473,16 +473,9 @@ export const selectDeviceAttrSchema: FormSchema[] = [ | ||
| 473 | field: 'keys', | 473 | field: 'keys', |
| 474 | label: '设备属性', | 474 | label: '设备属性', |
| 475 | component: 'Select', | 475 | component: 'Select', |
| 476 | - // componentProps: { | ||
| 477 | - // api: async ({ entityId }: { entityId: string }) => { | ||
| 478 | - // if (!entityId) return []; | ||
| 479 | - // try { | ||
| 480 | - // const res = await getDeviceDataKeys(entityId); | ||
| 481 | - // return res.map((item) => ({ label: item, value: item })); | ||
| 482 | - // } catch (error) {} | ||
| 483 | - // }, | ||
| 484 | - // immediate: true, | ||
| 485 | - // }, | 476 | + componentProps: { |
| 477 | + getPopupContainer: () => document.body, | ||
| 478 | + }, | ||
| 486 | }, | 479 | }, |
| 487 | ]; | 480 | ]; |
| 488 | 481 |
| 1 | import { Moment } from 'moment'; | 1 | import { Moment } from 'moment'; |
| 2 | import { getPacketIntervalByRange, getPacketIntervalByValue, intervalOption } from './helper'; | 2 | import { getPacketIntervalByRange, getPacketIntervalByValue, intervalOption } from './helper'; |
| 3 | import { FormSchema } from '/@/components/Form'; | 3 | import { FormSchema } from '/@/components/Form'; |
| 4 | +import { ColEx } from '/@/components/Form/src/types'; | ||
| 5 | +import { useGridLayout } from '/@/hooks/component/useGridLayout'; | ||
| 4 | export enum QueryWay { | 6 | export enum QueryWay { |
| 5 | LATEST = 'latest', | 7 | LATEST = 'latest', |
| 6 | TIME_PERIOD = 'timePeriod', | 8 | TIME_PERIOD = 'timePeriod', |
| @@ -49,6 +51,7 @@ export const defaultSchemas: FormSchema[] = [ | @@ -49,6 +51,7 @@ export const defaultSchemas: FormSchema[] = [ | ||
| 49 | }) | 51 | }) |
| 50 | : setFieldsValue({ [SchemaFiled.START_TS]: null }); | 52 | : setFieldsValue({ [SchemaFiled.START_TS]: null }); |
| 51 | }, | 53 | }, |
| 54 | + getPopupContainer: () => document.body, | ||
| 52 | }; | 55 | }; |
| 53 | }, | 56 | }, |
| 54 | }, | 57 | }, |
| @@ -66,6 +69,7 @@ export const defaultSchemas: FormSchema[] = [ | @@ -66,6 +69,7 @@ export const defaultSchemas: FormSchema[] = [ | ||
| 66 | onChange() { | 69 | onChange() { |
| 67 | setFieldsValue({ [SchemaFiled.INTERVAL]: null }); | 70 | setFieldsValue({ [SchemaFiled.INTERVAL]: null }); |
| 68 | }, | 71 | }, |
| 72 | + getPopupContainer: () => document.body, | ||
| 69 | }; | 73 | }; |
| 70 | }, | 74 | }, |
| 71 | rules: [{ required: true, message: '最后数据为必选项', type: 'number' }], | 75 | rules: [{ required: true, message: '最后数据为必选项', type: 'number' }], |
| @@ -97,11 +101,10 @@ export const defaultSchemas: FormSchema[] = [ | @@ -97,11 +101,10 @@ export const defaultSchemas: FormSchema[] = [ | ||
| 97 | dates = []; | 101 | dates = []; |
| 98 | setFieldsValue({ [SchemaFiled.INTERVAL]: null }); | 102 | setFieldsValue({ [SchemaFiled.INTERVAL]: null }); |
| 99 | }, | 103 | }, |
| 104 | + getPopupContainer: () => document.body, | ||
| 100 | }; | 105 | }; |
| 101 | }, | 106 | }, |
| 102 | - colProps: { | ||
| 103 | - span: 10, | ||
| 104 | - }, | 107 | + colProps: useGridLayout(2, 2, 2, 2, 2, 2) as unknown as ColEx, |
| 105 | }, | 108 | }, |
| 106 | { | 109 | { |
| 107 | field: SchemaFiled.AGG, | 110 | field: SchemaFiled.AGG, |
| @@ -137,6 +140,7 @@ export const defaultSchemas: FormSchema[] = [ | @@ -137,6 +140,7 @@ export const defaultSchemas: FormSchema[] = [ | ||
| 137 | } | 140 | } |
| 138 | return { | 141 | return { |
| 139 | options, | 142 | options, |
| 143 | + getPopupContainer: () => document.body, | ||
| 140 | }; | 144 | }; |
| 141 | }, | 145 | }, |
| 142 | }, | 146 | }, |
| @@ -152,6 +156,7 @@ export const defaultSchemas: FormSchema[] = [ | @@ -152,6 +156,7 @@ export const defaultSchemas: FormSchema[] = [ | ||
| 152 | return { | 156 | return { |
| 153 | max: 50000, | 157 | max: 50000, |
| 154 | min: 7, | 158 | min: 7, |
| 159 | + getPopupContainer: () => document.body, | ||
| 155 | }; | 160 | }; |
| 156 | }, | 161 | }, |
| 157 | }, | 162 | }, |