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 27 UPDATE_DATA_COMPONENT = '/data_component',
28 28 }
29 29
  30 +enum DataBoardShareUrl {
  31 + GET_DATA_COMPONENT = '/noauth/share/dataBoard',
  32 +}
  33 +
30 34 enum DeviceUrl {
31 35 GET_DEVICE = '/device/list/master',
32 36 GET_SLAVE_DEVICE = '/device/list/slave',
... ... @@ -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 160 * @description 获取所有主设备
145 161 * @param params
146 162 * @returns
... ...
... ... @@ -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;
... ...
... ... @@ -6,6 +6,7 @@ import {
6 6 EXCEPTION_COMPONENT,
7 7 PAGE_NOT_FOUND_NAME,
8 8 } from '/@/router/constant';
  9 +import { DATA_BOARD_SHARE_URL } from '/@/views/data/board/config/config';
9 10
10 11 // 404 on a page
11 12 export const PAGE_NOT_FOUND_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 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 3 import { mainOutRoutes } from './mainOut';
4 4 import { PageEnum } from '/@/enums/pageEnum';
5 5 import { t } from '/@/hooks/web/useI18n';
... ... @@ -85,4 +85,5 @@ export const basicRoutes = [
85 85 ...mainOutRoutes,
86 86 REDIRECT_ROUTE,
87 87 PAGE_NOT_FOUND_ROUTE,
  88 + DATA_BOARD_SHARE,
88 89 ];
... ...
... ... @@ -92,13 +92,15 @@
92 92 placement="topLeft"
93 93 :title="dateUtil(props?.value?.updateTime || new Date()).format(DEFAULT_DATE_FORMAT)"
94 94 >
95   - <span>更新时间:</span>
96   - <span
  95 + <div
97 96 :style="{ fontSize: fontSize({ radio: getRadio, basic: 12, max: 16 }) }"
98 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 104 </Tooltip>
103 105 </div>
104 106 </div>
... ...
1 1 <script lang="ts" setup>
2   - import { MoreOutlined, LineChartOutlined } from '@ant-design/icons-vue';
  2 + import { MoreOutlined } from '@ant-design/icons-vue';
3 3 import { DropMenu } from '/@/components/Dropdown';
4 4 import Dropdown from '/@/components/Dropdown/src/Dropdown.vue';
5 5 import { Tooltip } from 'ant-design-vue';
6 6 import SvgIcon from '/@/components/Icon/src/SvgIcon.vue';
7   - import { MoreActionEvent } from '../../config/config';
  7 + import { isBataBoardSharePage, MoreActionEvent } from '../../config/config';
8 8 import { computed } from '@vue/reactivity';
9 9 import { usePermission } from '/@/hooks/web/usePermission';
10 10 import { DataSource } from '/@/api/dataBoard/model';
  11 + import { useRoute } from 'vue-router';
11 12
12 13 const emit = defineEmits(['action']);
13 14 const props = defineProps<{
... ... @@ -41,6 +42,12 @@
41 42 return basicMenu;
42 43 });
43 44
  45 + const ROUTE = useRoute();
  46 +
  47 + const getIsSharePage = computed(() => {
  48 + return isBataBoardSharePage(ROUTE.path);
  49 + });
  50 +
44 51 const handleMenuEvent = (event: DropMenu) => {
45 52 emit('action', event, props.id);
46 53 };
... ... @@ -64,9 +71,7 @@
64 71 </div>
65 72 </div>
66 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 75 <Dropdown
71 76 v-if="dropMenuList.length"
72 77 :drop-menu-list="dropMenuList"
... ... @@ -74,7 +79,7 @@
74 79 @menu-event="handleMenuEvent"
75 80 >
76 81 <Tooltip title="更多">
77   - <MoreOutlined class="transform rotate-90 cursor-pointer" />
  82 + <MoreOutlined v-if="!getIsSharePage" class="transform rotate-90 cursor-pointer" />
78 83 </Tooltip>
79 84 </Dropdown>
80 85 </div>
... ...
... ... @@ -8,3 +8,22 @@ export enum MoreActionEvent {
8 8
9 9 export const DEFAULT_WIDGET_WIDTH = 6;
10 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 9 import { useModalInner } from '/@/components/Modal';
10 10 import { DataBoardLayoutInfo } from '../../types/type';
11 11 import { useMessage } from '/@/hooks/web/useMessage';
  12 + import { decode } from '../../config/config';
12 13
13 14 interface DataComponentRouteParams extends RouteParams {
14 15 id: string;
... ... @@ -21,7 +22,7 @@
21 22 const { createMessage } = useMessage();
22 23
23 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 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 23 :style="{ borderColor: props.controlId === props.checkedId ? '#1a74e8' : '#f0f0f0' }"
24 24 hoverable
25 25 bordered
26   - class="w-60 h-60 widget-select"
  26 + class="w-60 h-60 widget-select !bg-light-50"
27 27 @click="handleClick"
28 28 >
29 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 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 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';
... ... @@ -7,11 +8,18 @@
7 8 import { DropMenu } from '/@/components/Dropdown';
8 9 import DataBindModal from './components/DataBindModal.vue';
9 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 18 import {
12 19 addDataComponent,
13 20 deleteDataComponent,
14 21 getDataComponent,
  22 + getShareBoardComponentInfo,
15 23 updateDataBoardLayout,
16 24 } from '/@/api/dataBoard';
17 25 import { useRoute, useRouter } from 'vue-router';
... ... @@ -25,27 +33,43 @@
25 33 import Authority from '/@/components/Authority/src/Authority.vue';
26 34 import { useSocketConnect } from '../hook/useSocketConnect';
27 35 import { buildUUID } from '/@/utils/uuid';
  36 + import HistoryTrendModal from './components/HistoryTrendModal.vue';
28 37
29 38 const ROUTE = useRoute();
30 39
31 40 const ROUTER = useRouter();
32 41
33 42 const { createMessage, createConfirm } = useMessage();
  43 +
34 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 61 const widgetEl = new Map<string, Fn>();
39 62
40 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 68 const GirdLayoutColNum = 24;
46 69 const GridLayoutMargin = 10;
47 70
48 71 const handleBack = () => {
  72 + if (unref(getIsSharePage)) return;
49 73 ROUTER.go(-1);
50 74 };
51 75
... ... @@ -166,12 +190,36 @@
166 190
167 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 217 const loading = ref(false);
170 218 const getDataBoardComponent = async () => {
171 219 try {
172 220 // dataBoardList.value = [];
173 221 loading.value = true;
174   - const data = await getDataComponent(unref(getBoardId));
  222 + const data = await getDataBoradDetail();
175 223 dataBoardList.value = data.data.componentData.map((item) => {
176 224 const index = data.data.componentLayout.findIndex((each) => item.id === each.id);
177 225 let layout;
... ... @@ -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 300 onMounted(() => {
247 301 getDataBoardComponent();
248 302 });
... ... @@ -250,10 +304,18 @@
250 304
251 305 <template>
252 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 314 <template #extra>
255 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 319 </Authority>
258 320 </template>
259 321 <div>
... ... @@ -301,7 +363,17 @@
301 363 :record="item.record.dataSource"
302 364 :id="item.record.id"
303 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 377 </template>
306 378 <template #controls="{ record, add, remove, update }">
307 379 <component
... ... @@ -320,6 +392,7 @@
320 392 </Spin>
321 393 </section>
322 394 <DataBindModal @register="register" @submit="getDataBoardComponent" />
  395 + <HistoryTrendModal @register="registerHistoryDataModal" />
323 396 </section>
324 397 </template>
325 398
... ...
... ... @@ -100,7 +100,6 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) {
100 100 const transformSocketMessageItem = () => {
101 101 const messageList: SocketMessageItem[] = [];
102 102 let index = 0;
103   - console.log(unref(dataSourceRef));
104 103 unref(dataSourceRef).forEach((record, recordIndex) => {
105 104 const componentId = record.record.id;
106 105 record.record.dataSource.forEach((dataSource, dataSourceIndex) => {
... ...
... ... @@ -7,7 +7,7 @@
7 7 import { useMessage } from '/@/hooks/web/useMessage';
8 8 import Dropdown from '/@/components/Dropdown/src/Dropdown.vue';
9 9 import { DropMenu } from '/@/components/Dropdown';
10   - import { MoreActionEvent } from './config/config';
  10 + import { DATA_BOARD_SHARE_URL, MoreActionEvent } from './config/config';
11 11 import { useModal } from '/@/components/Modal';
12 12 import PanelDetailModal from './components/PanelDetailModal.vue';
13 13 import { getDataBoardList, deleteDataBoard } from '/@/api/dataBoard';
... ... @@ -18,6 +18,7 @@
18 18 import Authority from '/@/components/Authority/src/Authority.vue';
19 19 import { computed } from '@vue/reactivity';
20 20 import { usePermission } from '/@/hooks/web/usePermission';
  21 + import { encode } from './config/config';
21 22
22 23 const ListItem = List.Item;
23 24 const router = useRouter();
... ... @@ -50,9 +51,14 @@
50 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 59 const { clipboardRef } = useCopyToClipboard();
54 60 const handleCopyShareUrl = (record: DataBoardRecord) => {
55   - clipboardRef.value = record.openUrl;
  61 + clipboardRef.value = createShareUrl(record.id, record.tenantId, record.name);
56 62 unref(clipboardRef) ? createMessage.success('复制成功') : createMessage.error('未找到分享链接');
57 63 };
58 64
... ... @@ -128,7 +134,8 @@
128 134
129 135 const handleViewBoard = (record: DataBoardRecord) => {
130 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 141 const handlePagenationPosition = () => {
... ...