Commit 3c344ce5584aa40ed50cbe5e5481a4f552afe64a

Authored by xp.Huang
2 parents 3d0dd1ae 295b2782

Merge branch 'ww' into 'main'

feat: implment history trend && implment share data board && perf style

See merge request huang/yun-teng-iot-front!293
@@ -27,6 +27,10 @@ enum DataComponentUrl { @@ -27,6 +27,10 @@ enum DataComponentUrl {
27 UPDATE_DATA_COMPONENT = '/data_component', 27 UPDATE_DATA_COMPONENT = '/data_component',
28 } 28 }
29 29
  30 +enum DataBoardShareUrl {
  31 + GET_DATA_COMPONENT = '/noauth/share/dataBoard',
  32 +}
  33 +
30 enum DeviceUrl { 34 enum DeviceUrl {
31 GET_DEVICE = '/device/list/master', 35 GET_DEVICE = '/device/list/master',
32 GET_SLAVE_DEVICE = '/device/list/slave', 36 GET_SLAVE_DEVICE = '/device/list/slave',
@@ -141,6 +145,18 @@ export const updateDataComponent = (params: UpdateDataComponentParams) => { @@ -141,6 +145,18 @@ export const updateDataComponent = (params: UpdateDataComponentParams) => {
141 }; 145 };
142 146
143 /** 147 /**
  148 + * @description 分享组件信息
  149 + * @param params
  150 + * @returns
  151 + */
  152 +export const getShareBoardComponentInfo = (params: { boardId: string; tenantId: string }) => {
  153 + const { boardId, tenantId } = params;
  154 + return defHttp.get({
  155 + url: `${DataBoardShareUrl.GET_DATA_COMPONENT}/${boardId}/${tenantId}`,
  156 + });
  157 +};
  158 +
  159 +/**
144 * @description 获取所有主设备 160 * @description 获取所有主设备
145 * @param params 161 * @param params
146 * @returns 162 * @returns
@@ -77,7 +77,7 @@ export interface DataSource { @@ -77,7 +77,7 @@ export interface DataSource {
77 slaveDeviceId: string; 77 slaveDeviceId: string;
78 gatewayDevice: boolean; 78 gatewayDevice: boolean;
79 componentInfo: ComponentInfo; 79 componentInfo: ComponentInfo;
80 - deviceName?: string; 80 + deviceName: string;
81 81
82 // front usage 82 // front usage
83 uuid?: string; 83 uuid?: string;
@@ -6,6 +6,7 @@ import { @@ -6,6 +6,7 @@ import {
6 EXCEPTION_COMPONENT, 6 EXCEPTION_COMPONENT,
7 PAGE_NOT_FOUND_NAME, 7 PAGE_NOT_FOUND_NAME,
8 } from '/@/router/constant'; 8 } from '/@/router/constant';
  9 +import { DATA_BOARD_SHARE_URL } from '/@/views/data/board/config/config';
9 10
10 // 404 on a page 11 // 404 on a page
11 export const PAGE_NOT_FOUND_ROUTE: AppRouteRecordRaw = { 12 export const PAGE_NOT_FOUND_ROUTE: AppRouteRecordRaw = {
@@ -76,3 +77,13 @@ export const ERROR_LOG_ROUTE: AppRouteRecordRaw = { @@ -76,3 +77,13 @@ export const ERROR_LOG_ROUTE: AppRouteRecordRaw = {
76 }, 77 },
77 ], 78 ],
78 }; 79 };
  80 +
  81 +export const DATA_BOARD_SHARE: AppRouteRecordRaw = {
  82 + path: DATA_BOARD_SHARE_URL(),
  83 + name: 'dataBoardSharePage',
  84 + component: () => import('/@/views/data/board/detail/index.vue'),
  85 + meta: {
  86 + ignoreAuth: true,
  87 + title: '分享看板',
  88 + },
  89 +};
1 import type { AppRouteRecordRaw, AppRouteModule } from '/@/router/types'; 1 import type { AppRouteRecordRaw, AppRouteModule } from '/@/router/types';
2 -import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/routes/basic'; 2 +import { DATA_BOARD_SHARE, PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/routes/basic';
3 import { mainOutRoutes } from './mainOut'; 3 import { mainOutRoutes } from './mainOut';
4 import { PageEnum } from '/@/enums/pageEnum'; 4 import { PageEnum } from '/@/enums/pageEnum';
5 import { t } from '/@/hooks/web/useI18n'; 5 import { t } from '/@/hooks/web/useI18n';
@@ -85,4 +85,5 @@ export const basicRoutes = [ @@ -85,4 +85,5 @@ export const basicRoutes = [
85 ...mainOutRoutes, 85 ...mainOutRoutes,
86 REDIRECT_ROUTE, 86 REDIRECT_ROUTE,
87 PAGE_NOT_FOUND_ROUTE, 87 PAGE_NOT_FOUND_ROUTE,
  88 + DATA_BOARD_SHARE,
88 ]; 89 ];
@@ -92,13 +92,15 @@ @@ -92,13 +92,15 @@
92 placement="topLeft" 92 placement="topLeft"
93 :title="dateUtil(props?.value?.updateTime || new Date()).format(DEFAULT_DATE_FORMAT)" 93 :title="dateUtil(props?.value?.updateTime || new Date()).format(DEFAULT_DATE_FORMAT)"
94 > 94 >
95 - <span>更新时间:</span>  
96 - <span 95 + <div
97 :style="{ fontSize: fontSize({ radio: getRadio, basic: 12, max: 16 }) }" 96 :style="{ fontSize: fontSize({ radio: getRadio, basic: 12, max: 16 }) }"
98 class="truncate" 97 class="truncate"
99 > 98 >
100 - {{ dateUtil(props?.value?.updateTime || new Date()).format(DEFAULT_DATE_FORMAT) }}  
101 - </span> 99 + <span class="mr-1">更新时间:</span>
  100 + <span class="truncate">
  101 + {{ dateUtil(props?.value?.updateTime || new Date()).format(DEFAULT_DATE_FORMAT) }}
  102 + </span>
  103 + </div>
102 </Tooltip> 104 </Tooltip>
103 </div> 105 </div>
104 </div> 106 </div>
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 - import { MoreOutlined, LineChartOutlined } from '@ant-design/icons-vue'; 2 + import { MoreOutlined } from '@ant-design/icons-vue';
3 import { DropMenu } from '/@/components/Dropdown'; 3 import { DropMenu } from '/@/components/Dropdown';
4 import Dropdown from '/@/components/Dropdown/src/Dropdown.vue'; 4 import Dropdown from '/@/components/Dropdown/src/Dropdown.vue';
5 import { Tooltip } from 'ant-design-vue'; 5 import { Tooltip } from 'ant-design-vue';
6 import SvgIcon from '/@/components/Icon/src/SvgIcon.vue'; 6 import SvgIcon from '/@/components/Icon/src/SvgIcon.vue';
7 - import { MoreActionEvent } from '../../config/config'; 7 + import { isBataBoardSharePage, MoreActionEvent } from '../../config/config';
8 import { computed } from '@vue/reactivity'; 8 import { computed } from '@vue/reactivity';
9 import { usePermission } from '/@/hooks/web/usePermission'; 9 import { usePermission } from '/@/hooks/web/usePermission';
10 import { DataSource } from '/@/api/dataBoard/model'; 10 import { DataSource } from '/@/api/dataBoard/model';
  11 + import { useRoute } from 'vue-router';
11 12
12 const emit = defineEmits(['action']); 13 const emit = defineEmits(['action']);
13 const props = defineProps<{ 14 const props = defineProps<{
@@ -41,6 +42,12 @@ @@ -41,6 +42,12 @@
41 return basicMenu; 42 return basicMenu;
42 }); 43 });
43 44
  45 + const ROUTE = useRoute();
  46 +
  47 + const getIsSharePage = computed(() => {
  48 + return isBataBoardSharePage(ROUTE.path);
  49 + });
  50 +
44 const handleMenuEvent = (event: DropMenu) => { 51 const handleMenuEvent = (event: DropMenu) => {
45 emit('action', event, props.id); 52 emit('action', event, props.id);
46 }; 53 };
@@ -64,9 +71,7 @@ @@ -64,9 +71,7 @@
64 </div> 71 </div>
65 </div> 72 </div>
66 <div class="flex items-center w-10"> 73 <div class="flex items-center w-10">
67 - <Tooltip title="趋势">  
68 - <LineChartOutlined class="cursor-pointer mx-1" />  
69 - </Tooltip> 74 + <slot name="moreAction"></slot>
70 <Dropdown 75 <Dropdown
71 v-if="dropMenuList.length" 76 v-if="dropMenuList.length"
72 :drop-menu-list="dropMenuList" 77 :drop-menu-list="dropMenuList"
@@ -74,7 +79,7 @@ @@ -74,7 +79,7 @@
74 @menu-event="handleMenuEvent" 79 @menu-event="handleMenuEvent"
75 > 80 >
76 <Tooltip title="更多"> 81 <Tooltip title="更多">
77 - <MoreOutlined class="transform rotate-90 cursor-pointer" /> 82 + <MoreOutlined v-if="!getIsSharePage" class="transform rotate-90 cursor-pointer" />
78 </Tooltip> 83 </Tooltip>
79 </Dropdown> 84 </Dropdown>
80 </div> 85 </div>
@@ -8,3 +8,22 @@ export enum MoreActionEvent { @@ -8,3 +8,22 @@ export enum MoreActionEvent {
8 8
9 export const DEFAULT_WIDGET_WIDTH = 6; 9 export const DEFAULT_WIDGET_WIDTH = 6;
10 export const DEFAULT_WIDGET_HEIGHT = 6; 10 export const DEFAULT_WIDGET_HEIGHT = 6;
  11 +
  12 +export const DATA_BOARD_SHARE_URL = (
  13 + boardId = ':boardId',
  14 + tenantId = ':tenantId',
  15 + name = ':boardName?'
  16 +) => `/data/board/share/${boardId}/${tenantId}/${name}`;
  17 +
  18 +export const isBataBoardSharePage = (url: string) => {
  19 + const reg = /^\/data\/board\/share/g;
  20 + return reg.test(url);
  21 +};
  22 +
  23 +export const encode = (string: string) => {
  24 + return encodeURIComponent(string);
  25 +};
  26 +
  27 +export const decode = (string: string) => {
  28 + return decodeURIComponent(string);
  29 +};
@@ -9,6 +9,7 @@ @@ -9,6 +9,7 @@
9 import { useModalInner } from '/@/components/Modal'; 9 import { useModalInner } from '/@/components/Modal';
10 import { DataBoardLayoutInfo } from '../../types/type'; 10 import { DataBoardLayoutInfo } from '../../types/type';
11 import { useMessage } from '/@/hooks/web/useMessage'; 11 import { useMessage } from '/@/hooks/web/useMessage';
  12 + import { decode } from '../../config/config';
12 13
13 interface DataComponentRouteParams extends RouteParams { 14 interface DataComponentRouteParams extends RouteParams {
14 id: string; 15 id: string;
@@ -21,7 +22,7 @@ @@ -21,7 +22,7 @@
21 const { createMessage } = useMessage(); 22 const { createMessage } = useMessage();
22 23
23 const boardId = computed(() => { 24 const boardId = computed(() => {
24 - return (ROUTE.params as DataComponentRouteParams).id; 25 + return decode((ROUTE.params as DataComponentRouteParams).boardId as string);
25 }); 26 });
26 27
27 const frontId = ref(''); 28 const frontId = ref('');
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 + labelWidth: 120,
  94 + fieldMapToTime: [[SchemaFiled.DATE_RANGE, [SchemaFiled.START_TS, SchemaFiled.END_TS]]],
  95 + submitButtonOptions: {
  96 + loading: loading as unknown as boolean,
  97 + },
  98 + async submitFunc() {
  99 + // 表单验证
  100 + await method.validate();
  101 + const value = method.getFieldsValue();
  102 + const searchParams = getSearchParams(value);
  103 +
  104 + if (!hasDeviceAttr()) return;
  105 + // 发送请求
  106 + loading.value = true;
  107 + const res = await getDeviceHistoryInfo(searchParams);
  108 + // 判断数据对象是否为空
  109 + if (!Object.keys(res).length) {
  110 + isNull.value = false;
  111 + return;
  112 + } else {
  113 + isNull.value = true;
  114 + }
  115 + setChartOptions(res, value.keys);
  116 + loading.value = false;
  117 + },
  118 + });
  119 +
  120 + const getDeviceDataKey = async (deviceId: string) => {
  121 + if (!deviceId) return;
  122 + try {
  123 + deviceAttrs.value = (await getDeviceAttributes({ deviceId })) || [];
  124 + await nextTick();
  125 + method.updateSchema({
  126 + field: SchemaFiled.KEYS,
  127 + componentProps: {
  128 + options: unref(deviceAttrs).map((item) => ({ label: item, value: item })),
  129 + },
  130 + });
  131 + } catch (error) {}
  132 + };
  133 +
  134 + const handleModalOpen = async () => {
  135 + await nextTick();
  136 +
  137 + method.setFieldsValue({
  138 + [SchemaFiled.START_TS]: 1 * 24 * 60 * 60 * 1000,
  139 + [SchemaFiled.LIMIT]: 7,
  140 + [SchemaFiled.AGG]: AggregateDataEnum.NONE,
  141 + });
  142 +
  143 + if (!hasDeviceAttr()) return;
  144 +
  145 + const { deviceId } = method.getFieldsValue();
  146 +
  147 + const res = await getDeviceHistoryInfo({
  148 + entityId: deviceId,
  149 + keys: unref(deviceAttrs).join(),
  150 + startTs: Date.now() - 1 * 24 * 60 * 60 * 1000,
  151 + endTs: Date.now(),
  152 + agg: AggregateDataEnum.NONE,
  153 + });
  154 +
  155 + // 判断对象是否为空
  156 + if (!Object.keys(res).length) {
  157 + isNull.value = false;
  158 + return;
  159 + } else {
  160 + isNull.value = true;
  161 + }
  162 + setChartOptions(res);
  163 + };
  164 +
  165 + const generateDeviceOptions = (dataSource: DataSource[]) => {
  166 + const record: { [key: string]: boolean } = {};
  167 + const options: Record<'label' | 'value', string>[] = [];
  168 + for (const item of dataSource) {
  169 + const { deviceName, gatewayDevice, slaveDeviceId } = item;
  170 + let { deviceId } = item;
  171 + if (gatewayDevice) {
  172 + deviceId = slaveDeviceId;
  173 + }
  174 + if (record[deviceId]) continue;
  175 + options.push({
  176 + label: deviceName,
  177 + value: deviceId,
  178 + });
  179 + record[deviceId] = true;
  180 + }
  181 +
  182 + return options;
  183 + };
  184 +
  185 + const [registerModal] = useModalInner(async (dataSource: DataSource[]) => {
  186 + deviceAttrs.value = [];
  187 + loading.value = false;
  188 + const options = generateDeviceOptions(dataSource);
  189 + await nextTick();
  190 + method.updateSchema({
  191 + field: SchemaFiled.DEVICE_ID,
  192 + componentProps({ formActionType }) {
  193 + const { setFieldsValue } = formActionType;
  194 + return {
  195 + options,
  196 + onChange(value: string) {
  197 + getDeviceDataKey(value);
  198 + setFieldsValue({ [SchemaFiled.KEYS]: null });
  199 + },
  200 + };
  201 + },
  202 + });
  203 +
  204 + await handleModalOpen();
  205 + });
  206 +</script>
  207 +
  208 +<template>
  209 + <BasicModal @register="registerModal" :destroy-on-close="true" width="70%" title="历史趋势">
  210 + <section
  211 + class="flex flex-col p-4 h-full w-full min-w-7/10"
  212 + style="color: #f0f2f5; background-color: #f0f2f5"
  213 + >
  214 + <section class="bg-white my-3 p-2">
  215 + <BasicForm @register="register" />
  216 + </section>
  217 + <section class="bg-white p-3" style="min-hight: 350px">
  218 + <div v-show="isNull" ref="chartRef" :style="{ height: '350px', width: '100%' }">
  219 + <Loading :loading="loading" :absolute="true" />
  220 + </div>
  221 + <Empty description="暂无数据,请选择设备查询" v-show="!isNull" />
  222 + </section>
  223 + </section>
  224 + </BasicModal>
  225 +</template>
  226 +
  227 +<style scoped></style>
@@ -23,7 +23,7 @@ @@ -23,7 +23,7 @@
23 :style="{ borderColor: props.controlId === props.checkedId ? '#1a74e8' : '#f0f0f0' }" 23 :style="{ borderColor: props.controlId === props.checkedId ? '#1a74e8' : '#f0f0f0' }"
24 hoverable 24 hoverable
25 bordered 25 bordered
26 - class="w-60 h-60 widget-select" 26 + class="w-60 h-60 widget-select !bg-light-50"
27 @click="handleClick" 27 @click="handleClick"
28 > 28 >
29 <div class="widget-container"> 29 <div class="widget-container">
  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 <script lang="ts" setup> 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, RollbackOutlined } from '@ant-design/icons-vue';
3 import { GridItem, GridLayout } from 'vue3-grid-layout'; 4 import { GridItem, GridLayout } from 'vue3-grid-layout';
4 import { nextTick, onMounted, ref } from 'vue'; 5 import { nextTick, onMounted, ref } from 'vue';
5 import WidgetWrapper from '../components/WidgetWrapper/WidgetWrapper.vue'; 6 import WidgetWrapper from '../components/WidgetWrapper/WidgetWrapper.vue';
@@ -7,11 +8,18 @@ @@ -7,11 +8,18 @@
7 import { DropMenu } from '/@/components/Dropdown'; 8 import { DropMenu } from '/@/components/Dropdown';
8 import DataBindModal from './components/DataBindModal.vue'; 9 import DataBindModal from './components/DataBindModal.vue';
9 import { useModal } from '/@/components/Modal'; 10 import { useModal } from '/@/components/Modal';
10 - import { DEFAULT_WIDGET_HEIGHT, DEFAULT_WIDGET_WIDTH, MoreActionEvent } from '../config/config'; 11 + import {
  12 + decode,
  13 + DEFAULT_WIDGET_HEIGHT,
  14 + DEFAULT_WIDGET_WIDTH,
  15 + isBataBoardSharePage,
  16 + MoreActionEvent,
  17 + } from '../config/config';
11 import { 18 import {
12 addDataComponent, 19 addDataComponent,
13 deleteDataComponent, 20 deleteDataComponent,
14 getDataComponent, 21 getDataComponent,
  22 + getShareBoardComponentInfo,
15 updateDataBoardLayout, 23 updateDataBoardLayout,
16 } from '/@/api/dataBoard'; 24 } from '/@/api/dataBoard';
17 import { useRoute, useRouter } from 'vue-router'; 25 import { useRoute, useRouter } from 'vue-router';
@@ -25,27 +33,43 @@ @@ -25,27 +33,43 @@
25 import Authority from '/@/components/Authority/src/Authority.vue'; 33 import Authority from '/@/components/Authority/src/Authority.vue';
26 import { useSocketConnect } from '../hook/useSocketConnect'; 34 import { useSocketConnect } from '../hook/useSocketConnect';
27 import { buildUUID } from '/@/utils/uuid'; 35 import { buildUUID } from '/@/utils/uuid';
  36 + import HistoryTrendModal from './components/HistoryTrendModal.vue';
28 37
29 const ROUTE = useRoute(); 38 const ROUTE = useRoute();
30 39
31 const ROUTER = useRouter(); 40 const ROUTER = useRouter();
32 41
33 const { createMessage, createConfirm } = useMessage(); 42 const { createMessage, createConfirm } = useMessage();
  43 +
34 const getBoardId = computed(() => { 44 const getBoardId = computed(() => {
35 - return (ROUTE.params as { id: string }).id; 45 + return decode((ROUTE.params as { boardId: string }).boardId);
  46 + });
  47 +
  48 + const getDataBoardName = computed(() => {
  49 + return decode((ROUTE.params as { boardName: string }).boardName || '');
  50 + });
  51 +
  52 + const getSharePageParams = computed(() => {
  53 + const { boardId, tenantId } = ROUTE.params as { boardId: string; tenantId: string };
  54 + return { boardId: decode(boardId), tenantId: decode(tenantId) };
  55 + });
  56 +
  57 + const getIsSharePage = computed(() => {
  58 + return isBataBoardSharePage(ROUTE.path);
36 }); 59 });
37 60
38 const widgetEl = new Map<string, Fn>(); 61 const widgetEl = new Map<string, Fn>();
39 62
40 const dataBoardList = ref<DataBoardLayoutInfo[]>([]); 63 const dataBoardList = ref<DataBoardLayoutInfo[]>([]);
41 64
42 - const draggable = ref(true);  
43 - const resizable = ref(true); 65 + const draggable = ref(!unref(getIsSharePage));
  66 + const resizable = ref(!unref(getIsSharePage));
44 67
45 const GirdLayoutColNum = 24; 68 const GirdLayoutColNum = 24;
46 const GridLayoutMargin = 10; 69 const GridLayoutMargin = 10;
47 70
48 const handleBack = () => { 71 const handleBack = () => {
  72 + if (unref(getIsSharePage)) return;
49 ROUTER.go(-1); 73 ROUTER.go(-1);
50 }; 74 };
51 75
@@ -166,12 +190,36 @@ @@ -166,12 +190,36 @@
166 190
167 const { beginSendMessage } = useSocketConnect(dataBoardList); 191 const { beginSendMessage } = useSocketConnect(dataBoardList);
168 192
  193 + const getBasePageComponentData = async () => {
  194 + try {
  195 + return await getDataComponent(unref(getBoardId));
  196 + } catch (error) {}
  197 + return [];
  198 + };
  199 +
  200 + const getSharePageComponentData = async () => {
  201 + try {
  202 + const params = unref(getSharePageParams);
  203 + return await getShareBoardComponentInfo(params);
  204 + } catch (error) {}
  205 + return [];
  206 + };
  207 +
  208 + const getDataBoradDetail = async () => {
  209 + try {
  210 + return unref(getIsSharePage)
  211 + ? await getSharePageComponentData()
  212 + : await getBasePageComponentData();
  213 + } catch (error) {}
  214 + return [];
  215 + };
  216 +
169 const loading = ref(false); 217 const loading = ref(false);
170 const getDataBoardComponent = async () => { 218 const getDataBoardComponent = async () => {
171 try { 219 try {
172 // dataBoardList.value = []; 220 // dataBoardList.value = [];
173 loading.value = true; 221 loading.value = true;
174 - const data = await getDataComponent(unref(getBoardId)); 222 + const data = await getDataBoradDetail();
175 dataBoardList.value = data.data.componentData.map((item) => { 223 dataBoardList.value = data.data.componentData.map((item) => {
176 const index = data.data.componentLayout.findIndex((each) => item.id === each.id); 224 const index = data.data.componentLayout.findIndex((each) => item.id === each.id);
177 let layout; 225 let layout;
@@ -243,6 +291,12 @@ @@ -243,6 +291,12 @@
243 } 291 }
244 }; 292 };
245 293
  294 + const [registerHistoryDataModal, historyDataModalMethod] = useModal();
  295 +
  296 + const handleOpenHistroyDataModal = (record: DataSource[]) => {
  297 + historyDataModalMethod.openModal(true, record);
  298 + };
  299 +
246 onMounted(() => { 300 onMounted(() => {
247 getDataBoardComponent(); 301 getDataBoardComponent();
248 }); 302 });
@@ -250,10 +304,18 @@ @@ -250,10 +304,18 @@
250 304
251 <template> 305 <template>
252 <section class="bg-light-50 flex flex-col overflow-hidden h-full w-full"> 306 <section class="bg-light-50 flex flex-col overflow-hidden h-full w-full">
253 - <PageHeader title="水电表看板" @back="handleBack"> 307 + <PageHeader>
  308 + <template #title>
  309 + <div class="flex items-center">
  310 + <RollbackOutlined v-if="!getIsSharePage" class="mr-3" @click="handleBack" />
  311 + <span>{{ getDataBoardName }}</span>
  312 + </div>
  313 + </template>
254 <template #extra> 314 <template #extra>
255 <Authority value="api:yt:dataBoardDetail:post"> 315 <Authority value="api:yt:dataBoardDetail:post">
256 - <Button type="primary" @click="handleOpenCreatePanel">创建组件</Button> 316 + <Button v-if="!getIsSharePage" type="primary" @click="handleOpenCreatePanel"
  317 + >创建组件</Button
  318 + >
257 </Authority> 319 </Authority>
258 </template> 320 </template>
259 <div> 321 <div>
@@ -301,7 +363,17 @@ @@ -301,7 +363,17 @@
301 :record="item.record.dataSource" 363 :record="item.record.dataSource"
302 :id="item.record.id" 364 :id="item.record.id"
303 @action="handleMoreAction" 365 @action="handleMoreAction"
304 - /> 366 + >
  367 + <template #moreAction>
  368 + <Tooltip title="趋势">
  369 + <LineChartOutlined
  370 + v-if="!getIsSharePage"
  371 + class="cursor-pointer mx-1"
  372 + @click="handleOpenHistroyDataModal(item.record.dataSource)"
  373 + />
  374 + </Tooltip>
  375 + </template>
  376 + </BaseWidgetHeader>
305 </template> 377 </template>
306 <template #controls="{ record, add, remove, update }"> 378 <template #controls="{ record, add, remove, update }">
307 <component 379 <component
@@ -320,6 +392,7 @@ @@ -320,6 +392,7 @@
320 </Spin> 392 </Spin>
321 </section> 393 </section>
322 <DataBindModal @register="register" @submit="getDataBoardComponent" /> 394 <DataBindModal @register="register" @submit="getDataBoardComponent" />
  395 + <HistoryTrendModal @register="registerHistoryDataModal" />
323 </section> 396 </section>
324 </template> 397 </template>
325 398
@@ -100,7 +100,6 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) { @@ -100,7 +100,6 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) {
100 const transformSocketMessageItem = () => { 100 const transformSocketMessageItem = () => {
101 const messageList: SocketMessageItem[] = []; 101 const messageList: SocketMessageItem[] = [];
102 let index = 0; 102 let index = 0;
103 - console.log(unref(dataSourceRef));  
104 unref(dataSourceRef).forEach((record, recordIndex) => { 103 unref(dataSourceRef).forEach((record, recordIndex) => {
105 const componentId = record.record.id; 104 const componentId = record.record.id;
106 record.record.dataSource.forEach((dataSource, dataSourceIndex) => { 105 record.record.dataSource.forEach((dataSource, dataSourceIndex) => {
@@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
7 import { useMessage } from '/@/hooks/web/useMessage'; 7 import { useMessage } from '/@/hooks/web/useMessage';
8 import Dropdown from '/@/components/Dropdown/src/Dropdown.vue'; 8 import Dropdown from '/@/components/Dropdown/src/Dropdown.vue';
9 import { DropMenu } from '/@/components/Dropdown'; 9 import { DropMenu } from '/@/components/Dropdown';
10 - import { MoreActionEvent } from './config/config'; 10 + import { DATA_BOARD_SHARE_URL, MoreActionEvent } from './config/config';
11 import { useModal } from '/@/components/Modal'; 11 import { useModal } from '/@/components/Modal';
12 import PanelDetailModal from './components/PanelDetailModal.vue'; 12 import PanelDetailModal from './components/PanelDetailModal.vue';
13 import { getDataBoardList, deleteDataBoard } from '/@/api/dataBoard'; 13 import { getDataBoardList, deleteDataBoard } from '/@/api/dataBoard';
@@ -18,6 +18,7 @@ @@ -18,6 +18,7 @@
18 import Authority from '/@/components/Authority/src/Authority.vue'; 18 import Authority from '/@/components/Authority/src/Authority.vue';
19 import { computed } from '@vue/reactivity'; 19 import { computed } from '@vue/reactivity';
20 import { usePermission } from '/@/hooks/web/usePermission'; 20 import { usePermission } from '/@/hooks/web/usePermission';
  21 + import { encode } from './config/config';
21 22
22 const ListItem = List.Item; 23 const ListItem = List.Item;
23 const router = useRouter(); 24 const router = useRouter();
@@ -50,9 +51,14 @@ @@ -50,9 +51,14 @@
50 pageSize.value = size; 51 pageSize.value = size;
51 } 52 }
52 53
  54 + const createShareUrl = (boardId: string, tenantId: string, name: string) => {
  55 + const { origin } = location;
  56 + return `${origin}${DATA_BOARD_SHARE_URL(encode(boardId), encode(tenantId), encode(name))}`;
  57 + };
  58 +
53 const { clipboardRef } = useCopyToClipboard(); 59 const { clipboardRef } = useCopyToClipboard();
54 const handleCopyShareUrl = (record: DataBoardRecord) => { 60 const handleCopyShareUrl = (record: DataBoardRecord) => {
55 - clipboardRef.value = record.openUrl; 61 + clipboardRef.value = createShareUrl(record.id, record.tenantId, record.name);
56 unref(clipboardRef) ? createMessage.success('复制成功') : createMessage.error('未找到分享链接'); 62 unref(clipboardRef) ? createMessage.success('复制成功') : createMessage.error('未找到分享链接');
57 }; 63 };
58 64
@@ -128,7 +134,8 @@ @@ -128,7 +134,8 @@
128 134
129 const handleViewBoard = (record: DataBoardRecord) => { 135 const handleViewBoard = (record: DataBoardRecord) => {
130 const hasDetailPermission = hasPermission('api:yt:dataBoard:detail'); 136 const hasDetailPermission = hasPermission('api:yt:dataBoard:detail');
131 - if (hasDetailPermission) router.push(`/data/board/detail/${record.id}`); 137 + if (hasDetailPermission)
  138 + router.push(`/data/board/detail/${encode(record.id)}/${encode(record.name)}`);
132 }; 139 };
133 140
134 const handlePagenationPosition = () => { 141 const handlePagenationPosition = () => {