Commit 00889916c57db54eef779c37ee890124cca7706d

Authored by ww
1 parent 7685e1c8

feat: basic implement data board history trend

... ... @@ -77,7 +77,7 @@ export interface DataSource {
77 77 slaveDeviceId: string;
78 78 gatewayDevice: boolean;
79 79 componentInfo: ComponentInfo;
80   - deviceName?: string;
  80 + deviceName: string;
81 81
82 82 // front usage
83 83 uuid?: string;
... ...
... ... @@ -64,9 +64,7 @@
64 64 </div>
65 65 </div>
66 66 <div class="flex items-center w-10">
67   - <Tooltip title="趋势">
68   - <LineChartOutlined class="cursor-pointer mx-1" />
69   - </Tooltip>
  67 + <slot name="moreAction"></slot>
70 68 <Dropdown
71 69 v-if="dropMenuList.length"
72 70 :drop-menu-list="dropMenuList"
... ...
1   -<script setup lang="ts"></script>
2   -
3   -<template>
4   - <div>历史趋势</div>
5   -</template>
  1 +<script lang="ts" setup>
  2 + import moment from 'moment';
  3 + import { nextTick, Ref, ref, unref } from 'vue';
  4 + import { 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 { AggregateDataEnum, eChartOptions } from '/@/views/device/localtion/config.data';
  9 + import { useGridLayout } from '/@/hooks/component/useGridLayout';
  10 + import { ColEx } from '/@/components/Form/src/types';
  11 + import { DataSource } from '/@/api/dataBoard/model';
  12 + import { useForm, BasicForm } from '/@/components/Form';
  13 + import { formSchema, QueryWay, SchemaFiled } from '../config/historyTrend.config';
  14 + import { DEFAULT_DATE_FORMAT } from '../config/util';
  15 + import { Loading } from '/@/components/Loading';
  16 + import BasicModal from '/@/components/Modal/src/BasicModal.vue';
  17 + import { useModalInner } from '/@/components/Modal';
  18 + import { getDeviceAttributes } from '/@/api/dataBoard';
  19 +
  20 + defineEmits(['register']);
  21 +
  22 + const chartRef = ref();
  23 +
  24 + const deviceAttrs = ref<string[]>([]);
  25 +
  26 + const loading = ref(false);
  27 +
  28 + const isNull = ref(false);
  29 +
  30 + function getSearchParams(value: Recordable) {
  31 + const { startTs, endTs, interval, agg, limit, keys, way, deviceId } = value;
  32 + if (way === QueryWay.LATEST) {
  33 + return {
  34 + entityId: deviceId,
  35 + keys: keys ? keys : unref(deviceAttrs).join(),
  36 + startTs: moment().subtract(startTs, 'ms').valueOf(),
  37 + endTs: Date.now(),
  38 + interval,
  39 + agg,
  40 + limit,
  41 + };
  42 + } else {
  43 + return {
  44 + entityId: deviceId,
  45 + keys: keys ? keys : unref(deviceAttrs).join(),
  46 + startTs: moment(startTs).valueOf(),
  47 + endTs: moment(endTs).valueOf(),
  48 + interval,
  49 + agg,
  50 + limit,
  51 + };
  52 + }
  53 + }
  54 +
  55 + function hasDeviceAttr() {
  56 + if (!unref(deviceAttrs).length) {
  57 + return false;
  58 + } else {
  59 + return true;
  60 + }
  61 + }
  62 +
  63 + function setChartOptions(data, keys?) {
  64 + const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
  65 +
  66 + const dataArray: any[] = [];
  67 + for (const key in data) {
  68 + for (const item1 of data[key]) {
  69 + let { ts, value } = item1;
  70 + const time = dateUtil(ts).format(DEFAULT_DATE_FORMAT);
  71 + value = Number(value).toFixed(2);
  72 + dataArray.push([time, value, key]);
  73 + }
  74 + }
  75 + keys = keys ? [keys] : unref(deviceAttrs);
  76 + const series: any = keys.map((item) => {
  77 + return {
  78 + name: item,
  79 + type: 'line',
  80 + data: dataArray.filter((item1) => item1[2] === item),
  81 + };
  82 + });
  83 + // 设置数据
  84 + setOptions(eChartOptions(series, keys));
  85 + }
  86 +
  87 + const [register, method] = useForm({
  88 + schemas: formSchema,
  89 + baseColProps: useGridLayout(2, 3, 4) as unknown as ColEx,
  90 + rowProps: {
  91 + gutter: 10,
  92 + },
  93 + fieldMapToTime: [[SchemaFiled.DATE_RANGE, [SchemaFiled.START_TS, SchemaFiled.END_TS]]],
  94 + async submitFunc() {
  95 + // 表单验证
  96 + await method.validate();
  97 + const value = method.getFieldsValue();
  98 + const searchParams = getSearchParams(value);
  99 +
  100 + console.log(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 (deviceId: string) => {
  119 + if (!deviceId) return;
  120 + try {
  121 + deviceAttrs.value = (await getDeviceAttributes({ deviceId })) || [];
  122 + await nextTick();
  123 + method.updateSchema({
  124 + field: SchemaFiled.KEYS,
  125 + componentProps: {
  126 + options: unref(deviceAttrs).map((item) => ({ label: item, value: item })),
  127 + },
  128 + });
  129 + } catch (error) {}
  130 + };
  131 +
  132 + const handleModalOpen = async () => {
  133 + await nextTick();
  134 +
  135 + method.setFieldsValue({
  136 + [SchemaFiled.START_TS]: 1 * 24 * 60 * 60 * 1000,
  137 + [SchemaFiled.LIMIT]: 7,
  138 + [SchemaFiled.AGG]: AggregateDataEnum.NONE,
  139 + });
  140 +
  141 + if (!hasDeviceAttr()) return;
  142 +
  143 + const { deviceId } = method.getFieldsValue();
  144 +
  145 + const res = await getDeviceHistoryInfo({
  146 + entityId: deviceId,
  147 + keys: unref(deviceAttrs).join(),
  148 + startTs: Date.now() - 1 * 24 * 60 * 60 * 1000,
  149 + endTs: Date.now(),
  150 + agg: AggregateDataEnum.NONE,
  151 + });
  152 +
  153 + // 判断对象是否为空
  154 + if (!Object.keys(res).length) {
  155 + isNull.value = false;
  156 + return;
  157 + } else {
  158 + isNull.value = true;
  159 + }
  160 + setChartOptions(res);
  161 + };
  162 +
  163 + const generateDeviceOptions = (dataSource: DataSource[]) => {
  164 + const record: { [key: string]: boolean } = {};
  165 + const options: Record<'label' | 'value', string>[] = [];
  166 + for (const item of dataSource) {
  167 + const { deviceName, gatewayDevice, slaveDeviceId } = item;
  168 + let { deviceId } = item;
  169 + if (gatewayDevice) {
  170 + deviceId = slaveDeviceId;
  171 + }
  172 + if (record[deviceId]) continue;
  173 + options.push({
  174 + label: deviceName,
  175 + value: deviceId,
  176 + });
  177 + record[deviceId] = true;
  178 + }
  179 +
  180 + return options;
  181 + };
  182 +
  183 + const [registerModal] = useModalInner(async (dataSource: DataSource[]) => {
  184 + deviceAttrs.value = [];
  185 + loading.value = false;
  186 + const options = generateDeviceOptions(dataSource);
  187 + await nextTick();
  188 + method.updateSchema({
  189 + field: SchemaFiled.DEVICE_ID,
  190 + componentProps({ formActionType }) {
  191 + const { setFieldsValue } = formActionType;
  192 + return {
  193 + options,
  194 + onChange(value: string) {
  195 + getDeviceDataKey(value);
  196 + setFieldsValue({ [SchemaFiled.KEYS]: null });
  197 + },
  198 + };
  199 + },
  200 + });
  201 +
  202 + await handleModalOpen();
  203 + });
  204 +</script>
  205 +
  206 +<template>
  207 + <BasicModal @register="registerModal" :destroy-on-close="true" width="70%">
  208 + <section class="flex flex-col p-4 h-full w-full min-w-7/10" style="color: #f0f2f5">
  209 + <section class="bg-white p-3">
  210 + <BasicForm @register="register" />
  211 + </section>
  212 + <section class="bg-white p-3">
  213 + <div v-show="isNull" ref="chartRef" :style="{ height: '400px', width: '100%' }">
  214 + <Loading :loading="loading" :absolute="true" />
  215 + </div>
  216 + <Empty v-show="!isNull" />
  217 + </section>
  218 + </section>
  219 + </BasicModal>
  220 +</template>
  221 +
  222 +<style scoped></style>
... ...
  1 +import { Moment } from 'moment';
  2 +import { getDeviceAttributes } from '/@/api/dataBoard';
  3 +import { FormSchema } from '/@/components/Form';
  4 +import { ColEx } from '/@/components/Form/src/types';
  5 +import { useGridLayout } from '/@/hooks/component/useGridLayout';
  6 +import {
  7 + getPacketIntervalByRange,
  8 + getPacketIntervalByValue,
  9 + intervalOption,
  10 +} from '/@/views/device/localtion/cpns/TimePeriodForm/helper';
  11 +export enum QueryWay {
  12 + LATEST = 'latest',
  13 + TIME_PERIOD = 'timePeriod',
  14 +}
  15 +
  16 +export enum SchemaFiled {
  17 + DEVICE_ID = 'deviceId',
  18 + WAY = 'way',
  19 + TIME_PERIOD = 'timePeriod',
  20 + KEYS = 'keys',
  21 + DATE_RANGE = 'dataRange',
  22 + START_TS = 'startTs',
  23 + END_TS = 'endTs',
  24 + INTERVAL = 'interval',
  25 + LIMIT = 'limit',
  26 + AGG = 'agg',
  27 + ORDER_BY = 'orderBy',
  28 +}
  29 +
  30 +export enum AggregateDataEnum {
  31 + MIN = 'MIN',
  32 + MAX = 'MAX',
  33 + AVG = 'AVG',
  34 + SUM = 'SUM',
  35 + COUNT = 'COUNT',
  36 + NONE = 'NONE',
  37 +}
  38 +export const formSchema: FormSchema[] = [
  39 + {
  40 + field: SchemaFiled.DEVICE_ID,
  41 + label: '设备名称',
  42 + component: 'Select',
  43 + rules: [{ required: true, message: '设备名称为必选项', type: 'string' }],
  44 + componentProps({ formActionType }) {
  45 + const { setFieldsValue } = formActionType;
  46 + return {
  47 + placeholder: '请选择设备',
  48 + onChange() {
  49 + setFieldsValue({ [SchemaFiled.KEYS]: null });
  50 + },
  51 + };
  52 + },
  53 + },
  54 + {
  55 + field: SchemaFiled.WAY,
  56 + label: '查询方式',
  57 + component: 'RadioGroup',
  58 + defaultValue: QueryWay.LATEST,
  59 + componentProps({ formActionType }) {
  60 + const { setFieldsValue } = formActionType;
  61 + return {
  62 + options: [
  63 + { label: '最后', value: QueryWay.LATEST },
  64 + { label: '时间段', value: QueryWay.TIME_PERIOD },
  65 + ],
  66 + onChange(value) {
  67 + value === QueryWay.LATEST
  68 + ? setFieldsValue({
  69 + [SchemaFiled.DATE_RANGE]: [],
  70 + [SchemaFiled.START_TS]: null,
  71 + [SchemaFiled.END_TS]: null,
  72 + })
  73 + : setFieldsValue({ [SchemaFiled.START_TS]: null });
  74 + },
  75 + getPopupContainer: () => document.body,
  76 + };
  77 + },
  78 + },
  79 + {
  80 + field: SchemaFiled.START_TS,
  81 + label: '最后数据',
  82 + component: 'Select',
  83 + ifShow({ values }) {
  84 + return values[SchemaFiled.WAY] === QueryWay.LATEST;
  85 + },
  86 + componentProps({ formActionType }) {
  87 + const { setFieldsValue } = formActionType;
  88 + return {
  89 + options: intervalOption,
  90 + onChange() {
  91 + setFieldsValue({ [SchemaFiled.INTERVAL]: null });
  92 + },
  93 + getPopupContainer: () => document.body,
  94 + };
  95 + },
  96 + rules: [{ required: true, message: '最后数据为必选项', type: 'number' }],
  97 + },
  98 + {
  99 + field: SchemaFiled.DATE_RANGE,
  100 + label: '时间段',
  101 + component: 'RangePicker',
  102 + ifShow({ values }) {
  103 + return values[SchemaFiled.WAY] === QueryWay.TIME_PERIOD;
  104 + },
  105 + rules: [{ required: true, message: '时间段为必选项' }],
  106 + componentProps({ formActionType }) {
  107 + const { setFieldsValue } = formActionType;
  108 + let dates: Moment[] = [];
  109 + return {
  110 + showTime: true,
  111 + onCalendarChange(value: Moment[]) {
  112 + dates = value;
  113 + },
  114 + disabledDate(current: Moment) {
  115 + if (!dates || dates.length === 0 || !current) {
  116 + return false;
  117 + }
  118 + const diffDate = current.diff(dates[0], 'years', true);
  119 + return Math.abs(diffDate) > 1;
  120 + },
  121 + onChange() {
  122 + dates = [];
  123 + setFieldsValue({ [SchemaFiled.INTERVAL]: null });
  124 + },
  125 + getPopupContainer: () => document.body,
  126 + };
  127 + },
  128 + colProps: useGridLayout(2, 2, 2, 2, 2, 2) as unknown as ColEx,
  129 + },
  130 + {
  131 + field: SchemaFiled.AGG,
  132 + label: '数据聚合功能',
  133 + component: 'Select',
  134 + // defaultValue: AggregateDataEnum.NONE,
  135 + componentProps: {
  136 + getPopupContainer: () => document.body,
  137 + options: [
  138 + { label: '最小值', value: AggregateDataEnum.MIN },
  139 + { label: '最大值', value: AggregateDataEnum.MAX },
  140 + { label: '平均值', value: AggregateDataEnum.AVG },
  141 + { label: '求和', value: AggregateDataEnum.SUM },
  142 + { label: '计数', value: AggregateDataEnum.COUNT },
  143 + { label: '空', value: AggregateDataEnum.NONE },
  144 + ],
  145 + },
  146 + },
  147 + {
  148 + field: SchemaFiled.INTERVAL,
  149 + label: '分组间隔',
  150 + component: 'Select',
  151 + ifShow({ values }) {
  152 + return values[SchemaFiled.AGG] !== AggregateDataEnum.NONE;
  153 + },
  154 + componentProps({ formModel, formActionType }) {
  155 + const options =
  156 + formModel[SchemaFiled.WAY] === QueryWay.LATEST
  157 + ? getPacketIntervalByValue(formModel[SchemaFiled.START_TS])
  158 + : getPacketIntervalByRange(formModel[SchemaFiled.DATE_RANGE]);
  159 + if (formModel[SchemaFiled.AGG] !== AggregateDataEnum.NONE) {
  160 + formActionType.setFieldsValue({ [SchemaFiled.LIMIT]: null });
  161 + }
  162 + return {
  163 + options,
  164 + getPopupContainer: () => document.body,
  165 + };
  166 + },
  167 + },
  168 + {
  169 + field: SchemaFiled.LIMIT,
  170 + label: '最大值',
  171 + component: 'InputNumber',
  172 + // defaultValue: 7,
  173 + ifShow({ values }) {
  174 + return values[SchemaFiled.AGG] === AggregateDataEnum.NONE;
  175 + },
  176 + componentProps() {
  177 + return {
  178 + max: 50000,
  179 + min: 7,
  180 + getPopupContainer: () => document.body,
  181 + };
  182 + },
  183 + },
  184 + {
  185 + field: SchemaFiled.KEYS,
  186 + label: '设备属性',
  187 + component: 'Select',
  188 + componentProps: {
  189 + getPopupContainer: () => document.body,
  190 + },
  191 + // componentProps({ formModel }) {
  192 + // const deviceId = formModel[SchemaFiled.DEVICE_ID];
  193 + // return {
  194 + // api: async () => {
  195 + // if (deviceId) {
  196 + // try {
  197 + // try {
  198 + // const data = await getDeviceAttributes({ deviceId });
  199 + // if (data) return data.map((item) => ({ label: item, value: item }));
  200 + // } catch (error) {}
  201 + // return [];
  202 + // } catch (error) {}
  203 + // }
  204 + // return [];
  205 + // },
  206 + // getPopupContainer: () => document.body,
  207 + // };
  208 + // },
  209 + },
  210 +];
  211 +
  212 +// export const selectDeviceAttrSchema: FormSchema[] = [
  213 +// {
  214 +// field: 'keys',
  215 +// label: '设备属性',
  216 +// component: 'Select',
  217 +// componentProps: {
  218 +// getPopupContainer: () => document.body,
  219 +// },
  220 +// },
  221 +// ];
... ...
1 1 <script lang="ts" setup>
2   - import { Button, PageHeader, Empty, Spin } from 'ant-design-vue';
  2 + import { Button, PageHeader, Empty, Spin, Tooltip } from 'ant-design-vue';
  3 + import { LineChartOutlined } from '@ant-design/icons-vue';
3 4 import { GridItem, GridLayout } from 'vue3-grid-layout';
4 5 import { nextTick, onMounted, ref } from 'vue';
5 6 import WidgetWrapper from '../components/WidgetWrapper/WidgetWrapper.vue';
... ... @@ -25,6 +26,7 @@
25 26 import Authority from '/@/components/Authority/src/Authority.vue';
26 27 import { useSocketConnect } from '../hook/useSocketConnect';
27 28 import { buildUUID } from '/@/utils/uuid';
  29 + import HistoryTrendModal from './components/HistoryTrendModal.vue';
28 30
29 31 const ROUTE = useRoute();
30 32
... ... @@ -243,6 +245,12 @@
243 245 }
244 246 };
245 247
  248 + const [registerHistoryDataModal, historyDataModalMethod] = useModal();
  249 +
  250 + const handleOpenHistroyDataModal = (record: DataSource[]) => {
  251 + historyDataModalMethod.openModal(true, record);
  252 + };
  253 +
246 254 onMounted(() => {
247 255 getDataBoardComponent();
248 256 });
... ... @@ -301,7 +309,16 @@
301 309 :record="item.record.dataSource"
302 310 :id="item.record.id"
303 311 @action="handleMoreAction"
304   - />
  312 + >
  313 + <template #moreAction>
  314 + <Tooltip title="趋势">
  315 + <LineChartOutlined
  316 + class="cursor-pointer mx-1"
  317 + @click="handleOpenHistroyDataModal(item.record.dataSource)"
  318 + />
  319 + </Tooltip>
  320 + </template>
  321 + </BaseWidgetHeader>
305 322 </template>
306 323 <template #controls="{ record, add, remove, update }">
307 324 <component
... ... @@ -320,6 +337,7 @@
320 337 </Spin>
321 338 </section>
322 339 <DataBindModal @register="register" @submit="getDataBoardComponent" />
  340 + <HistoryTrendModal @register="registerHistoryDataModal" />
323 341 </section>
324 342 </template>
325 343
... ...