Commit aeecc329698aef25e2f2d9b3ae36617e56556078

Authored by ww
2 parents be9da75c 6d8d3f9e

merge: 合并代码解决冲突

Showing 150 changed files with 8851 additions and 606 deletions
... ... @@ -5,7 +5,9 @@
5 5 "public/resource/tinymce/langs"
6 6 ],
7 7 "cSpell.words": [
  8 + "Cmds",
8 9 "COAP",
  10 + "echarts",
9 11 "edrx",
10 12 "EFENTO",
11 13 "inited",
... ... @@ -17,6 +19,8 @@
17 19 "unref",
18 20 "vben",
19 21 "VITE",
  22 + "vnode",
  23 + "vueuse",
20 24 "windicss"
21 25 ]
22 26 }
... ...
1 1 <script lang="ts" setup>
2 2 import '@simonwep/pickr/dist/themes/monolith.min.css';
3 3 import ColorPicker from '@simonwep/pickr';
4   - import type { PropType } from 'vue';
  4 + import { PropType, watch } from 'vue';
5 5 import { computed, onMounted, onUnmounted, ref, unref } from 'vue';
6 6
7 7 type Format = Exclude<keyof ColorPicker.HSVaColor, 'clone'>;
... ... @@ -18,10 +18,6 @@
18 18 config: {
19 19 type: Object as PropType<ColorPicker.Options>,
20 20 },
21   - defaultValue: {
22   - type: String,
23   - default: '',
24   - },
25 21 });
26 22
27 23 const emit = defineEmits(['update:value']);
... ... @@ -52,11 +48,16 @@
52 48 };
53 49
54 50 const onClear = () => {
55   - emit('update:value', props.defaultValue);
  51 + emit('update:value', props.value);
56 52 unref(picker)?.hide();
57   - unref(picker)?.setColor(props.defaultValue);
  53 + props.value && unref(picker)?.setColor(props.value);
58 54 };
59 55
  56 + // const onChange = () => {
  57 + // const value = getColor();
  58 + // emit('update:value', value);
  59 + // };
  60 +
60 61 const getOption = computed<ColorPicker.Options>(() => {
61 62 const { config = {} } = props;
62 63 return {
... ... @@ -100,17 +101,28 @@
100 101 };
101 102 });
102 103
  104 + watch(
  105 + () => props.value,
  106 + (value) => {
  107 + if (value) {
  108 + unref(picker)?.setColor(value);
  109 + }
  110 + }
  111 + );
  112 +
103 113 onMounted(() => {
104 114 picker.value = ColorPicker.create(unref(getOption));
105 115 unref(picker)?.on('init', onInit);
106 116 unref(picker)?.on('save', onSave);
107 117 unref(picker)?.on('clear', onClear);
  118 + // unref(picker)?.on('change', onChange);
108 119 });
109 120
110 121 onUnmounted(() => {
111 122 unref(picker)?.off('init', onInit);
112 123 unref(picker)?.off('save', onSave);
113 124 unref(picker)?.off('clear', onClear);
  125 + // unref(picker)?.off('change', onChange);
114 126
115 127 unref(picker)?.destroyAndRemove();
116 128 });
... ...
... ... @@ -3,6 +3,7 @@ export enum DataActionModeEnum {
3 3 READ = 'READ',
4 4 UPDATE = 'UPDATE',
5 5 DELETE = 'DELETE',
  6 + COPY = 'COPY',
6 7 }
7 8
8 9 export enum DataActionModeNameEnum {
... ...
  1 +import { onUnmounted, ref } from 'vue';
  2 +import { isFunction } from '/@/utils/is';
  3 +
  4 +interface ScriptOptions {
  5 + src: string;
  6 + onLoad?: Fn;
  7 + onError?: Fn;
  8 +}
  9 +
  10 +export enum InjectScriptStatusEnum {
  11 + DONE = 'DONE',
  12 + LOAD = 'LOAD',
  13 + SUCCESS = 'SUCCESS',
  14 + ERROR = 'ERROR',
  15 +}
  16 +
  17 +export function useInjectScript(opts: ScriptOptions) {
  18 + const loading = ref(false);
  19 + const status = ref(InjectScriptStatusEnum.DONE);
  20 + let script: HTMLScriptElement;
  21 +
  22 + const toInject = () => {
  23 + loading.value = true;
  24 + const { onError, onLoad } = opts;
  25 + status.value = InjectScriptStatusEnum.LOAD;
  26 + return new Promise((resolve, reject) => {
  27 + script = document.createElement('script');
  28 + script.type = 'text/javascript';
  29 + script.onload = function () {
  30 + loading.value = false;
  31 + status.value = InjectScriptStatusEnum.SUCCESS;
  32 + onLoad && isFunction(onLoad) && onLoad();
  33 + resolve('');
  34 + };
  35 +
  36 + script.onerror = function (err) {
  37 + loading.value = false;
  38 + status.value = InjectScriptStatusEnum.ERROR;
  39 + onError && isFunction(onError) && onError();
  40 + reject(err);
  41 + };
  42 +
  43 + script.src = opts.src;
  44 + document.head.appendChild(script);
  45 + });
  46 + };
  47 +
  48 + onUnmounted(() => {
  49 + script && script.remove();
  50 + });
  51 +
  52 + return {
  53 + loading,
  54 + status,
  55 + toInject,
  56 + };
  57 +}
... ...
... ... @@ -102,7 +102,10 @@ export const basicSchema: FormSchema[] = [
102 102 },
103 103 ];
104 104
105   -export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): FormSchema[] => {
  105 +export const dataSourceSchema = (isEdit: boolean, frontId?: string): FormSchema[] => {
  106 + // console.log(useSelectWidgetKeys());
  107 + // const isEdit = unref(useSelectWidgetMode()) === DataActionModeEnum.UPDATE;
  108 +
106 109 return [
107 110 {
108 111 field: DataSourceField.IS_GATEWAY_DEVICE,
... ...
1 1 <script lang="ts" setup>
2   - import { Button, PageHeader, Empty, Spin, Tooltip } from 'ant-design-vue';
3   - import { GridItem, GridLayout } from 'vue3-grid-layout';
4   - import { nextTick, onMounted, ref } from 'vue';
5   - import WidgetWrapper from '../components/WidgetWrapper/WidgetWrapper.vue';
6   - import BaseWidgetHeader from '../components/WidgetHeader/BaseWidgetHeader.vue';
7   - import { DropMenu } from '/@/components/Dropdown';
8   - import DataBindModal from './components/DataBindModal.vue';
9   - import { useModal } from '/@/components/Modal';
10   - import {
11   - decode,
12   - DEFAULT_MAX_COL,
13   - DEFAULT_MIN_HEIGHT,
14   - DEFAULT_MIN_WIDTH,
15   - DEFAULT_WIDGET_HEIGHT,
16   - DEFAULT_WIDGET_WIDTH,
17   - MoreActionEvent,
18   - VisualComponentPermission,
19   - } from '../config/config';
20   - import {
21   - addDataComponent,
22   - deleteDataComponent,
23   - getDataComponent,
24   - updateDataBoardLayout,
25   - } from '/@/api/dataBoard';
26   - import { useRoute, useRouter } from 'vue-router';
27   - import { computed, unref } from '@vue/reactivity';
28   - import {
29   - ComponentInfoDetail,
30   - DataComponentRecord,
31   - DataSource,
32   - Layout,
33   - } from '/@/api/dataBoard/model';
34   - import { frontComponentMap } from '../components/help';
35   - import { calcScale } from './config/util';
36   - import { useMessage } from '/@/hooks/web/useMessage';
37   - import { DataBoardLayoutInfo } from '../types/type';
38   - import Authority from '/@/components/Authority/src/Authority.vue';
39   - import { useSocketConnect } from '../hook/useSocketConnect';
40   - import { buildUUID } from '/@/utils/uuid';
41   - import HistoryTrendModal from './components/HistoryTrendModal.vue';
42   - import trendIcon from '/@/assets/svg/trend.svg';
43   - import backIcon from '/@/assets/images/back.png';
44   - import backWhiteIcon from '/@/assets/images/backWhite.png';
45   - import { useCalcGridLayout } from '../hook/useCalcGridLayout';
46   - import { FrontComponent } from '../const/const';
47   - import { useScript } from '/@/hooks/web/useScript';
48   - import { BAI_DU_MAP_GL_LIB, BAI_DU_MAP_TRACK_ANIMATION } from '/@/utils/fnUtils';
49   - import { useAppStore } from '/@/store/modules/app';
50   - import { useRole } from '/@/hooks/business/useRole';
51   -
52   - const userStore = useAppStore();
53   -
54   - const getAceClass = computed((): string => userStore.getDarkMode);
55   -
56   - const props = defineProps<{
57   - value?: Recordable;
58   - }>();
59   -
60   - const ROUTE = useRoute();
61   -
62   - const ROUTER = useRouter();
63   -
64   - const { isCustomerUser } = useRole();
65   - const { toPromise: injectBaiDuMapLib } = useScript({ src: BAI_DU_MAP_GL_LIB });
66   - const { toPromise: injectBaiDuMapTrackAniMationLib } = useScript({
67   - src: BAI_DU_MAP_TRACK_ANIMATION,
68   - });
69   -
70   - const { createMessage, createConfirm } = useMessage();
71   -
72   - const getBoardId = computed(() => {
73   - return decode((ROUTE.params as { boardId: string }).boardId);
74   - });
75   -
76   - const getDataBoardName = computed(() => {
77   - return decode((ROUTE.params as { boardName: string }).boardName || '');
78   - });
79   -
80   - const getSharePageData = computed(() => {
81   - return props.value!;
82   - });
83   -
84   - const getIsSharePage = computed(() => {
85   - return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId');
86   - });
87   -
88   - const widgetEl = new Map<string, Fn>();
89   -
90   - const dataBoardList = ref<DataBoardLayoutInfo[]>([]);
91   -
92   - const draggable = ref(!unref(getIsSharePage));
93   - const resizable = ref(!unref(getIsSharePage));
94   -
95   - const GirdLayoutColNum = DEFAULT_MAX_COL;
96   - const GridLayoutMargin = 20;
97   -
98   - const handleBack = () => {
99   - if (unref(getIsSharePage)) return;
100   - ROUTER.go(-1);
101   - };
102   -
103   - function updateSize(i: string, _newH: number, _newW: number, newHPx: number, newWPx: number) {
104   - newWPx = Number(newWPx);
105   - newHPx = Number(newHPx);
106   -
107   - const data = dataBoardList.value.find((item) => item.i === i)!;
108   - const length = data.record.dataSource.length || 0;
109   -
110   - const row = Math.floor(Math.pow(length, 0.5));
111   - const col = Math.floor(length / row);
112   - let width = Math.floor(100 / col);
113   - let height = Math.floor(100 / row);
114   -
115   - const WHRatio = newWPx / newHPx;
116   - const HWRatio = newHPx / newWPx;
117   -
118   - if (WHRatio > 1.6) {
119   - width = Math.floor(100 / length);
120   - height = 100;
121   - }
122   -
123   - if (HWRatio > 1.6) {
124   - height = Math.floor(100 / length);
125   - width = 100;
126   - }
127   -
128   - data.width = newWPx;
129   - data.height = newHPx;
130   -
131   - data.record.dataSource = data.record.dataSource.map((item) => {
132   - if (!item.uuid) item.uuid = buildUUID();
133   - return {
134   - ...item,
135   - width,
136   - height,
137   - radio: calcScale(newWPx, newHPx, width, height),
138   - };
139   - });
140   -
141   - nextTick(() => {
142   - const updateFn = widgetEl.get(i);
143   - if (updateFn) updateFn();
144   - });
145   - }
146   -
147   - const itemResize = (i: string, newH: number, newW: number, newHPx: number, newWPx: number) => {
148   - updateSize(i, newH, newW, newHPx, newWPx);
149   - };
150   -
151   - const itemResized = (i: string, newH: number, newW: number, newHPx: number, newWPx: number) => {
152   - handleSaveLayoutInfo();
153   - updateSize(i, newH, newW, newHPx, newWPx);
154   - };
155   -
156   - const itemContainerResized = (
157   - i: string,
158   - newH: number,
159   - newW: number,
160   - newHPx: number,
161   - newWPx: number
162   - ) => {
163   - updateSize(i, newH, newW, newHPx, newWPx);
164   - };
165   -
166   - const itemMoved = (i: string) => {
167   - handleSaveLayoutInfo();
168   - updateCharts(i);
169   - };
170   -
171   - const updateCharts = (i: string) => {
172   - nextTick(() => {
173   - const updateFn = widgetEl.get(i);
174   - if (updateFn) updateFn();
175   - });
176   - };
177   -
178   - const setComponentRef = (el: Element, record: DataBoardLayoutInfo) => {
179   - if (widgetEl.has(record.i)) widgetEl.delete(record.i);
180   - if (el && (el as unknown as { update: Fn }).update)
181   - widgetEl.set(record.i, (el as unknown as { update: Fn }).update);
182   - };
183   -
184   - const [register, { openModal }] = useModal();
185   -
186   - const handleMoreAction = (event: DropMenu, id: string) => {
187   - if (event.event === MoreActionEvent.DELETE) {
188   - createConfirm({
189   - iconType: 'warning',
190   - content: '是否确认删除?',
191   - onOk: () => handleDelete(id),
192   - });
193   - }
194   - if (event.event === MoreActionEvent.EDIT) handleUpdate(id);
195   - if (event.event === MoreActionEvent.COPY) handleCopy(id);
196   - };
197   -
198   - const handleOpenCreatePanel = () => {
199   - openModal(true, { isEdit: false });
200   - };
201   -
202   - const getLayoutInfo = () => {
203   - return unref(dataBoardList).map((item) => {
204   - return {
205   - id: item.i,
206   - h: item.h,
207   - w: item.w,
208   - x: item.x,
209   - y: item.y,
210   - } as Layout;
211   - });
212   - };
213   -
214   - const handleSaveLayoutInfo = async () => {
215   - try {
216   - await updateDataBoardLayout({
217   - boardId: unref(getBoardId),
218   - layout: getLayoutInfo(),
219   - });
220   - } catch (error) {}
221   - };
222   -
223   - const { beginSendMessage } = useSocketConnect(dataBoardList);
224   -
225   - const getBasePageComponentData = async () => {
226   - try {
227   - return await getDataComponent(unref(getBoardId));
228   - } catch (error) {}
229   - return {} as ComponentInfoDetail;
230   - };
231   -
232   - const getDataBoradDetail = async () => {
233   - try {
234   - return unref(getIsSharePage) ? unref(getSharePageData) : await getBasePageComponentData();
235   - } catch (error) {
236   - return {} as ComponentInfoDetail;
237   - }
238   - };
239   -
240   - const loading = ref(false);
241   - const getDataBoardComponent = async () => {
242   - try {
243   - dataBoardList.value = [];
244   - loading.value = true;
245   - const data = await getDataBoradDetail();
246   -
247   - if (!data.data.componentData) {
248   - dataBoardList.value = [];
249   - return;
250   - }
251   - dataBoardList.value = data.data.componentData.map((item) => {
252   - const index = data.data.componentLayout.findIndex((each) => item.id === each.id);
253   - let layout;
254   - if (!~index) {
255   - layout = {};
256   - } else {
257   - layout = data.data.componentLayout[index];
258   - }
259   - return {
260   - i: item.id,
261   - w: layout.w || DEFAULT_WIDGET_WIDTH,
262   - h: layout.h || DEFAULT_WIDGET_HEIGHT,
263   - x: layout.x || 0,
264   - y: layout.y || 0,
265   - record: item,
266   - };
267   - });
268   - beginSendMessage();
269   - } catch (error) {
270   - throw error;
271   - } finally {
272   - loading.value = false;
273   - }
274   - };
275   -
276   - const handleUpdateComponent = async (id: string) => {
277   - try {
278   - loading.value = true;
279   - const data = await getDataBoradDetail();
280   - const updateIndex = data.data.componentData.findIndex((item) => item.id === id);
281   - const originalIndex = unref(dataBoardList).findIndex((item) => item.i === id);
282   -
283   - const newUpdateData = data.data.componentData[updateIndex];
284   - const originalData = unref(dataBoardList)[originalIndex];
285   - dataBoardList.value[originalIndex] = {
286   - i: id,
287   - w: originalData.w || DEFAULT_WIDGET_WIDTH,
288   - h: originalData.h || DEFAULT_WIDGET_HEIGHT,
289   - x: originalData.x || 0,
290   - y: originalData.y || 0,
291   - width: originalData.width,
292   - height: originalData.height,
293   - record: newUpdateData,
294   - };
295   -
296   - updateSize(id, 0, 0, originalData.height || 0, originalData.width || 0);
297   -
298   - beginSendMessage();
299   - } catch (error) {
300   - } finally {
301   - loading.value = false;
302   - }
303   - };
304   -
305   - const getComponent = (record: DataComponentRecord) => {
306   - const frontComponent = record.frontId;
307   - const component = frontComponentMap.get(frontComponent as FrontComponent);
308   - return component?.Component;
309   - };
310   -
311   - const getComponentConfig = (
312   - record: DataBoardLayoutInfo['record'],
313   - dataSourceRecord: DataSource | DataSource[]
314   - ) => {
315   - const frontComponent = record.frontId;
316   - const component = frontComponentMap.get(frontComponent as FrontComponent);
317   - return component?.transformConfig(component.ComponentConfig || {}, record, dataSourceRecord);
318   - };
319   -
320   - const handleUpdate = async (id: string) => {
321   - const record = unref(dataBoardList).find((item) => item.i === id);
322   - openModal(true, { isEdit: true, record });
323   - };
324   -
325   - const { calcLayoutInfo } = useCalcGridLayout();
326   -
327   - const handleCopy = async (id: string) => {
328   - const record = unref(dataBoardList).find((item) => item.i === id);
329   - try {
330   - const data = await addDataComponent({
331   - boardId: unref(getBoardId),
332   - record: {
333   - dataBoardId: unref(getBoardId),
334   - frontId: record?.record.frontId,
335   - name: record?.record.name,
336   - remark: record?.record.remark,
337   - dataSource: record?.record.dataSource,
338   - },
339   - });
340   - createMessage.success('复制成功');
341   - const _id = data.data.id;
342   - const layoutInfo = getLayoutInfo();
343   -
344   - const newGridLayout = calcLayoutInfo(unref(dataBoardList), {
345   - width: record?.w || DEFAULT_WIDGET_HEIGHT,
346   - height: record?.h || DEFAULT_WIDGET_HEIGHT,
347   - });
348   - layoutInfo.push({
349   - id: _id,
350   - ...newGridLayout,
351   - } as Layout);
352   -
353   - await updateDataBoardLayout({
354   - boardId: unref(getBoardId),
355   - layout: layoutInfo,
356   - });
357   -
358   - await getDataBoardComponent();
359   - } catch (error) {}
360   - };
361   -
362   - const handleDelete = async (id: string) => {
363   - try {
364   - const dataBoardId = unref(dataBoardList).find((item) => item.i == id)?.record.dataBoardId;
365   - if (!dataBoardId) return;
366   - await deleteDataComponent({ dataBoardId, ids: [id] });
367   - createMessage.success('删除成功');
368   - await getDataBoardComponent();
369   - } catch (error) {}
370   - };
371   -
372   - const [registerHistoryDataModal, historyDataModalMethod] = useModal();
373   -
374   - const handleOpenHistroyDataModal = (record: DataSource[]) => {
375   - historyDataModalMethod.openModal(true, record);
376   - };
377   -
378   - const hasHistoryTrend = (item: DataBoardLayoutInfo) => {
379   - return frontComponentMap.get(item.record.frontId as FrontComponent)?.hasHistoryTrend;
380   - };
381   -
382   - onMounted(async () => {
383   - injectBaiDuMapLib();
384   - injectBaiDuMapTrackAniMationLib();
385   - getDataBoardComponent();
386   - });
  2 + import { Palette } from '/@/views/visual/palette';
387 3 </script>
388   -
389 4 <template>
390   - <section class="flex flex-col overflow-hidden h-full w-full board-detail">
391   - <PageHeader v-if="!getIsSharePage">
392   - <template #title>
393   - <div class="flex items-center">
394   - <img
395   - :src="getAceClass === 'dark' ? backWhiteIcon : backIcon"
396   - v-if="!getIsSharePage"
397   - class="mr-3 cursor-pointer"
398   - @click="handleBack"
399   - />
400   - <span class="text-lg" color="#333">{{ getDataBoardName }}</span>
401   - </div>
402   - </template>
403   - <template #extra>
404   - <Authority :value="VisualComponentPermission.CREATE">
405   - <Button
406   - v-if="!getIsSharePage && !isCustomerUser"
407   - type="primary"
408   - @click="handleOpenCreatePanel"
409   - >
410   - 创建组件
411   - </Button>
412   - </Authority>
413   - </template>
414   - <div>
415   - <span class="mr-3 text-sm" style="color: #666">已创建组件:</span>
416   - <span style="color: #409eff"> {{ dataBoardList.length }}个</span>
417   - </div>
418   - </PageHeader>
419   - <section class="flex-1">
420   - <Spin :spinning="loading">
421   - <GridLayout
422   - v-model:layout="dataBoardList"
423   - :col-num="GirdLayoutColNum"
424   - :row-height="30"
425   - :margin="[GridLayoutMargin, GridLayoutMargin]"
426   - :is-draggable="draggable"
427   - :is-resizable="resizable"
428   - :vertical-compact="true"
429   - :use-css-transforms="true"
430   - style="width: 100%"
431   - >
432   - <GridItem
433   - v-for="item in dataBoardList"
434   - :key="item.i"
435   - :static="item.static"
436   - :x="item.x"
437   - :y="item.y"
438   - :w="item.w"
439   - :h="item.h"
440   - :i="item.i"
441   - :min-h="DEFAULT_MIN_HEIGHT"
442   - :min-w="DEFAULT_MIN_WIDTH"
443   - :style="{ display: 'flex', flexWrap: 'wrap' }"
444   - class="grid-item-layout"
445   - @resized="itemResized"
446   - @resize="itemResize"
447   - @moved="itemMoved"
448   - @container-resized="itemContainerResized"
449   - drag-ignore-from=".no-drag"
450   - >
451   - <WidgetWrapper
452   - :key="item.i"
453   - :ref="(el: Element) => setComponentRef(el, item)"
454   - :record="item.record"
455   - :data-source="item.record.dataSource"
456   - >
457   - <template #header>
458   - <BaseWidgetHeader
459   - :record="item.record.dataSource"
460   - :id="item.record.id"
461   - :panel-name="item.record.name"
462   - @action="handleMoreAction"
463   - >
464   - <template #moreAction>
465   - <Tooltip v-if="!isCustomerUser" title="趋势">
466   - <img
467   - :src="trendIcon"
468   - v-if="!getIsSharePage && hasHistoryTrend(item)"
469   - class="cursor-pointer w-4.5 h-4.5"
470   - @click="handleOpenHistroyDataModal(item.record.dataSource)"
471   - />
472   - </Tooltip>
473   - </template>
474   - </BaseWidgetHeader>
475   - </template>
476   - <template #controls="{ record, add, remove, update }">
477   - <component
478   - :is="getComponent(item.record)"
479   - :add="add"
480   - :remove="remove"
481   - :update="update"
482   - :radio="record.radio || {}"
483   - v-bind="getComponentConfig(item.record, record)"
484   - :random="false"
485   - />
486   - </template>
487   - </WidgetWrapper>
488   - </GridItem>
489   - </GridLayout>
490   - <Empty
491   - v-if="!dataBoardList.length"
492   - class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
493   - />
494   - </Spin>
495   - </section>
496   - <DataBindModal
497   - :layout="dataBoardList"
498   - @register="register"
499   - @update="handleUpdateComponent"
500   - @create="getDataBoardComponent"
501   - />
502   - <HistoryTrendModal @register="registerHistoryDataModal" />
503   - </section>
  5 + <Palette />
504 6 </template>
505   -
506   -<style lang="less" scoped>
507   - .vue-grid-item:not(.vue-grid-placeholder) {
508   - background: #fff;
509   - border: none !important;
510   -
511   - /* border: 1px solid black; */
512   - }
513   -
514   - .vue-grid-item .resizing {
515   - opacity: 0.9;
516   - }
517   -
518   - .vue-grid-item .static {
519   - background: #cce;
520   - }
521   -
522   - .vue-grid-item .text {
523   - font-size: 24px;
524   - text-align: center;
525   - position: absolute;
526   - top: 0;
527   - bottom: 0;
528   - left: 0;
529   - right: 0;
530   - margin: auto;
531   - height: 100%;
532   - width: 100%;
533   - }
534   -
535   - .vue-grid-item .no-drag {
536   - height: 100%;
537   - width: 100%;
538   - }
539   -
540   - .vue-grid-item .minMax {
541   - font-size: 12px;
542   - }
543   -
544   - .vue-grid-item .add {
545   - cursor: pointer;
546   - }
547   -
548   - .vue-draggable-handle {
549   - position: absolute;
550   - width: 20px;
551   - height: 20px;
552   - top: 0;
553   - left: 0;
554   - background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><circle cx='5' cy='5' r='5' fill='#999999'/></svg>")
555   - no-repeat;
556   - background-position: bottom right;
557   - padding: 0 8px 8px 0;
558   - background-repeat: no-repeat;
559   - background-origin: content-box;
560   - box-sizing: border-box;
561   - cursor: pointer;
562   - }
563   -
564   - .grid-item-layout {
565   - overflow: hidden;
566   - border: 1px solid #eee !important;
567   - background-color: #fcfcfc !important;
568   - }
569   -
570   - .board-detail:deep(.ant-page-header) {
571   - padding: 20px 20px 0 20px;
572   - }
573   -
574   - .board-detail:deep(.ant-page-header-heading) {
575   - height: 78px;
576   - padding: 0 20px 0 20px;
577   - box-sizing: border-box;
578   - background-color: #fff;
579   - }
580   -
581   - [data-theme='dark'] .board-detail:deep(.ant-page-header-heading) {
582   - @apply bg-dark-900;
583   - }
584   -
585   - .board-detail:deep(.ant-page-header-heading-extra) {
586   - margin: 0;
587   - line-height: 78px;
588   - }
589   -
590   - .board-detail:deep(.ant-page-header-content) {
591   - padding-top: 20px;
592   - }
593   -
594   - :deep(.vue-resizable-handle) {
595   - z-index: 99;
596   - }
597   -</style>
... ...
  1 +<script lang="ts" setup>
  2 + import { Button, PageHeader, Empty, Spin, Tooltip } from 'ant-design-vue';
  3 + import { GridItem, GridLayout } from 'vue3-grid-layout';
  4 + import { nextTick, onMounted, ref } from 'vue';
  5 + import WidgetWrapper from '../components/WidgetWrapper/WidgetWrapper.vue';
  6 + import BaseWidgetHeader from '../components/WidgetHeader/BaseWidgetHeader.vue';
  7 + import { DropMenu } from '/@/components/Dropdown';
  8 + import DataBindModal from './components/DataBindModal.vue';
  9 + import { useModal } from '/@/components/Modal';
  10 + import {
  11 + decode,
  12 + DEFAULT_MAX_COL,
  13 + DEFAULT_MIN_HEIGHT,
  14 + DEFAULT_MIN_WIDTH,
  15 + DEFAULT_WIDGET_HEIGHT,
  16 + DEFAULT_WIDGET_WIDTH,
  17 + MoreActionEvent,
  18 + VisualComponentPermission,
  19 + } from '../config/config';
  20 + import {
  21 + addDataComponent,
  22 + deleteDataComponent,
  23 + getDataComponent,
  24 + updateDataBoardLayout,
  25 + } from '/@/api/dataBoard';
  26 + import { useRoute, useRouter } from 'vue-router';
  27 + import { computed, unref } from '@vue/reactivity';
  28 + import {
  29 + ComponentInfoDetail,
  30 + DataComponentRecord,
  31 + DataSource,
  32 + Layout,
  33 + } from '/@/api/dataBoard/model';
  34 + import { frontComponentMap } from '../components/help';
  35 + import { calcScale } from './config/util';
  36 + import { useMessage } from '/@/hooks/web/useMessage';
  37 + import { DataBoardLayoutInfo } from '../types/type';
  38 + import Authority from '/@/components/Authority/src/Authority.vue';
  39 + import { useSocketConnect } from '../hook/useSocketConnect';
  40 + import { buildUUID } from '/@/utils/uuid';
  41 + import HistoryTrendModal from './components/HistoryTrendModal.vue';
  42 + import trendIcon from '/@/assets/svg/trend.svg';
  43 + import backIcon from '/@/assets/images/back.png';
  44 + import backWhiteIcon from '/@/assets/images/backWhite.png';
  45 + import { useCalcGridLayout } from '../hook/useCalcGridLayout';
  46 + import { FrontComponent } from '../const/const';
  47 + import { useScript } from '/@/hooks/web/useScript';
  48 + import { BAI_DU_MAP_GL_LIB, BAI_DU_MAP_TRACK_ANIMATION } from '/@/utils/fnUtils';
  49 + import { useAppStore } from '/@/store/modules/app';
  50 + import { useRole } from '/@/hooks/business/useRole';
  51 +
  52 + const userStore = useAppStore();
  53 +
  54 + const getAceClass = computed((): string => userStore.getDarkMode);
  55 +
  56 + const props = defineProps<{
  57 + value?: Recordable;
  58 + }>();
  59 +
  60 + const ROUTE = useRoute();
  61 +
  62 + const ROUTER = useRouter();
  63 +
  64 + const { isCustomerUser } = useRole();
  65 + const { toPromise: injectBaiDuMapLib } = useScript({ src: BAI_DU_MAP_GL_LIB });
  66 + const { toPromise: injectBaiDuMapTrackAniMationLib } = useScript({
  67 + src: BAI_DU_MAP_TRACK_ANIMATION,
  68 + });
  69 +
  70 + const { createMessage, createConfirm } = useMessage();
  71 +
  72 + const getBoardId = computed(() => {
  73 + return decode((ROUTE.params as { boardId: string }).boardId);
  74 + });
  75 +
  76 + const getDataBoardName = computed(() => {
  77 + return decode((ROUTE.params as { boardName: string }).boardName || '');
  78 + });
  79 +
  80 + const getSharePageData = computed(() => {
  81 + return props.value!;
  82 + });
  83 +
  84 + const getIsSharePage = computed(() => {
  85 + return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId');
  86 + });
  87 +
  88 + const widgetEl = new Map<string, Fn>();
  89 +
  90 + const dataBoardList = ref<DataBoardLayoutInfo[]>([]);
  91 +
  92 + const draggable = ref(!unref(getIsSharePage));
  93 + const resizable = ref(!unref(getIsSharePage));
  94 +
  95 + const GirdLayoutColNum = DEFAULT_MAX_COL;
  96 + const GridLayoutMargin = 20;
  97 +
  98 + const handleBack = () => {
  99 + if (unref(getIsSharePage)) return;
  100 + ROUTER.go(-1);
  101 + };
  102 +
  103 + function updateSize(i: string, _newH: number, _newW: number, newHPx: number, newWPx: number) {
  104 + newWPx = Number(newWPx);
  105 + newHPx = Number(newHPx);
  106 +
  107 + const data = dataBoardList.value.find((item) => item.i === i)!;
  108 + const length = data.record.dataSource.length || 0;
  109 +
  110 + const row = Math.floor(Math.pow(length, 0.5));
  111 + const col = Math.floor(length / row);
  112 + let width = Math.floor(100 / col);
  113 + let height = Math.floor(100 / row);
  114 +
  115 + const WHRatio = newWPx / newHPx;
  116 + const HWRatio = newHPx / newWPx;
  117 +
  118 + if (WHRatio > 1.6) {
  119 + width = Math.floor(100 / length);
  120 + height = 100;
  121 + }
  122 +
  123 + if (HWRatio > 1.6) {
  124 + height = Math.floor(100 / length);
  125 + width = 100;
  126 + }
  127 +
  128 + data.width = newWPx;
  129 + data.height = newHPx;
  130 +
  131 + data.record.dataSource = data.record.dataSource.map((item) => {
  132 + if (!item.uuid) item.uuid = buildUUID();
  133 + return {
  134 + ...item,
  135 + width,
  136 + height,
  137 + radio: calcScale(newWPx, newHPx, width, height),
  138 + };
  139 + });
  140 +
  141 + nextTick(() => {
  142 + const updateFn = widgetEl.get(i);
  143 + if (updateFn) updateFn();
  144 + });
  145 + }
  146 +
  147 + const itemResize = (i: string, newH: number, newW: number, newHPx: number, newWPx: number) => {
  148 + updateSize(i, newH, newW, newHPx, newWPx);
  149 + };
  150 +
  151 + const itemResized = (i: string, newH: number, newW: number, newHPx: number, newWPx: number) => {
  152 + handleSaveLayoutInfo();
  153 + updateSize(i, newH, newW, newHPx, newWPx);
  154 + };
  155 +
  156 + const itemContainerResized = (
  157 + i: string,
  158 + newH: number,
  159 + newW: number,
  160 + newHPx: number,
  161 + newWPx: number
  162 + ) => {
  163 + updateSize(i, newH, newW, newHPx, newWPx);
  164 + };
  165 +
  166 + const itemMoved = (i: string) => {
  167 + handleSaveLayoutInfo();
  168 + updateCharts(i);
  169 + };
  170 +
  171 + const updateCharts = (i: string) => {
  172 + nextTick(() => {
  173 + const updateFn = widgetEl.get(i);
  174 + if (updateFn) updateFn();
  175 + });
  176 + };
  177 +
  178 + const setComponentRef = (el: Element, record: DataBoardLayoutInfo) => {
  179 + if (widgetEl.has(record.i)) widgetEl.delete(record.i);
  180 + if (el && (el as unknown as { update: Fn }).update)
  181 + widgetEl.set(record.i, (el as unknown as { update: Fn }).update);
  182 + };
  183 +
  184 + const [register, { openModal }] = useModal();
  185 +
  186 + const handleMoreAction = (event: DropMenu, id: string) => {
  187 + if (event.event === MoreActionEvent.DELETE) {
  188 + createConfirm({
  189 + iconType: 'warning',
  190 + content: '是否确认删除?',
  191 + onOk: () => handleDelete(id),
  192 + });
  193 + }
  194 + if (event.event === MoreActionEvent.EDIT) handleUpdate(id);
  195 + if (event.event === MoreActionEvent.COPY) handleCopy(id);
  196 + };
  197 +
  198 + const handleOpenCreatePanel = () => {
  199 + openModal(true, { isEdit: false });
  200 + };
  201 +
  202 + const getLayoutInfo = () => {
  203 + return unref(dataBoardList).map((item) => {
  204 + return {
  205 + id: item.i,
  206 + h: item.h,
  207 + w: item.w,
  208 + x: item.x,
  209 + y: item.y,
  210 + } as Layout;
  211 + });
  212 + };
  213 +
  214 + const handleSaveLayoutInfo = async () => {
  215 + try {
  216 + await updateDataBoardLayout({
  217 + boardId: unref(getBoardId),
  218 + layout: getLayoutInfo(),
  219 + });
  220 + } catch (error) {}
  221 + };
  222 +
  223 + const { beginSendMessage } = useSocketConnect(dataBoardList);
  224 +
  225 + const getBasePageComponentData = async () => {
  226 + try {
  227 + return await getDataComponent(unref(getBoardId));
  228 + } catch (error) {}
  229 + return {} as ComponentInfoDetail;
  230 + };
  231 +
  232 + const getDataBoradDetail = async () => {
  233 + try {
  234 + return unref(getIsSharePage) ? unref(getSharePageData) : await getBasePageComponentData();
  235 + } catch (error) {
  236 + return {} as ComponentInfoDetail;
  237 + }
  238 + };
  239 +
  240 + const loading = ref(false);
  241 + const getDataBoardComponent = async () => {
  242 + try {
  243 + dataBoardList.value = [];
  244 + loading.value = true;
  245 + const data = await getDataBoradDetail();
  246 +
  247 + if (!data.data.componentData) {
  248 + dataBoardList.value = [];
  249 + return;
  250 + }
  251 + dataBoardList.value = data.data.componentData.map((item) => {
  252 + const index = data.data.componentLayout.findIndex((each) => item.id === each.id);
  253 + let layout;
  254 + if (!~index) {
  255 + layout = {};
  256 + } else {
  257 + layout = data.data.componentLayout[index];
  258 + }
  259 + return {
  260 + i: item.id,
  261 + w: layout.w || DEFAULT_WIDGET_WIDTH,
  262 + h: layout.h || DEFAULT_WIDGET_HEIGHT,
  263 + x: layout.x || 0,
  264 + y: layout.y || 0,
  265 + record: item,
  266 + };
  267 + });
  268 + beginSendMessage();
  269 + } catch (error) {
  270 + throw error;
  271 + } finally {
  272 + loading.value = false;
  273 + }
  274 + };
  275 +
  276 + const handleUpdateComponent = async (id: string) => {
  277 + try {
  278 + loading.value = true;
  279 + const data = await getDataBoradDetail();
  280 + const updateIndex = data.data.componentData.findIndex((item) => item.id === id);
  281 + const originalIndex = unref(dataBoardList).findIndex((item) => item.i === id);
  282 +
  283 + const newUpdateData = data.data.componentData[updateIndex];
  284 + const originalData = unref(dataBoardList)[originalIndex];
  285 + dataBoardList.value[originalIndex] = {
  286 + i: id,
  287 + w: originalData.w || DEFAULT_WIDGET_WIDTH,
  288 + h: originalData.h || DEFAULT_WIDGET_HEIGHT,
  289 + x: originalData.x || 0,
  290 + y: originalData.y || 0,
  291 + width: originalData.width,
  292 + height: originalData.height,
  293 + record: newUpdateData,
  294 + };
  295 +
  296 + updateSize(id, 0, 0, originalData.height || 0, originalData.width || 0);
  297 +
  298 + beginSendMessage();
  299 + } catch (error) {
  300 + } finally {
  301 + loading.value = false;
  302 + }
  303 + };
  304 +
  305 + const getComponent = (record: DataComponentRecord) => {
  306 + const frontComponent = record.frontId;
  307 + const component = frontComponentMap.get(frontComponent as FrontComponent);
  308 + return component?.Component;
  309 + };
  310 +
  311 + const getComponentConfig = (
  312 + record: DataBoardLayoutInfo['record'],
  313 + dataSourceRecord: DataSource | DataSource[]
  314 + ) => {
  315 + const frontComponent = record.frontId;
  316 + const component = frontComponentMap.get(frontComponent as FrontComponent);
  317 + return component?.transformConfig(component.ComponentConfig || {}, record, dataSourceRecord);
  318 + };
  319 +
  320 + const handleUpdate = async (id: string) => {
  321 + const record = unref(dataBoardList).find((item) => item.i === id);
  322 + openModal(true, { isEdit: true, record });
  323 + };
  324 +
  325 + const { calcLayoutInfo } = useCalcGridLayout();
  326 +
  327 + const handleCopy = async (id: string) => {
  328 + const record = unref(dataBoardList).find((item) => item.i === id);
  329 + try {
  330 + const data = await addDataComponent({
  331 + boardId: unref(getBoardId),
  332 + record: {
  333 + dataBoardId: unref(getBoardId),
  334 + frontId: record?.record.frontId,
  335 + name: record?.record.name,
  336 + remark: record?.record.remark,
  337 + dataSource: record?.record.dataSource,
  338 + },
  339 + });
  340 + createMessage.success('复制成功');
  341 + const _id = data.data.id;
  342 + const layoutInfo = getLayoutInfo();
  343 +
  344 + const newGridLayout = calcLayoutInfo(unref(dataBoardList), {
  345 + width: record?.w || DEFAULT_WIDGET_HEIGHT,
  346 + height: record?.h || DEFAULT_WIDGET_HEIGHT,
  347 + });
  348 + layoutInfo.push({
  349 + id: _id,
  350 + ...newGridLayout,
  351 + } as Layout);
  352 +
  353 + await updateDataBoardLayout({
  354 + boardId: unref(getBoardId),
  355 + layout: layoutInfo,
  356 + });
  357 +
  358 + await getDataBoardComponent();
  359 + } catch (error) {}
  360 + };
  361 +
  362 + const handleDelete = async (id: string) => {
  363 + try {
  364 + const dataBoardId = unref(dataBoardList).find((item) => item.i == id)?.record.dataBoardId;
  365 + if (!dataBoardId) return;
  366 + await deleteDataComponent({ dataBoardId, ids: [id] });
  367 + createMessage.success('删除成功');
  368 + await getDataBoardComponent();
  369 + } catch (error) {}
  370 + };
  371 +
  372 + const [registerHistoryDataModal, historyDataModalMethod] = useModal();
  373 +
  374 + const handleOpenHistroyDataModal = (record: DataSource[]) => {
  375 + historyDataModalMethod.openModal(true, record);
  376 + };
  377 +
  378 + const hasHistoryTrend = (item: DataBoardLayoutInfo) => {
  379 + return frontComponentMap.get(item.record.frontId as FrontComponent)?.hasHistoryTrend;
  380 + };
  381 +
  382 + onMounted(async () => {
  383 + injectBaiDuMapLib();
  384 + injectBaiDuMapTrackAniMationLib();
  385 + getDataBoardComponent();
  386 + });
  387 +</script>
  388 +
  389 +<template>
  390 + <section class="flex flex-col overflow-hidden h-full w-full board-detail">
  391 + <PageHeader v-if="!getIsSharePage">
  392 + <template #title>
  393 + <div class="flex items-center">
  394 + <img
  395 + :src="getAceClass === 'dark' ? backWhiteIcon : backIcon"
  396 + v-if="!getIsSharePage"
  397 + class="mr-3 cursor-pointer"
  398 + @click="handleBack"
  399 + />
  400 + <span class="text-lg" color="#333">{{ getDataBoardName }}</span>
  401 + </div>
  402 + </template>
  403 + <template #extra>
  404 + <Authority :value="VisualComponentPermission.CREATE">
  405 + <Button
  406 + v-if="!getIsSharePage && !isCustomerUser"
  407 + type="primary"
  408 + @click="handleOpenCreatePanel"
  409 + >
  410 + 创建组件
  411 + </Button>
  412 + </Authority>
  413 + </template>
  414 + <div>
  415 + <span class="mr-3 text-sm" style="color: #666">已创建组件:</span>
  416 + <span style="color: #409eff"> {{ dataBoardList.length }}个</span>
  417 + </div>
  418 + </PageHeader>
  419 + <section class="flex-1">
  420 + <Spin :spinning="loading">
  421 + <GridLayout
  422 + v-model:layout="dataBoardList"
  423 + :col-num="GirdLayoutColNum"
  424 + :row-height="30"
  425 + :margin="[GridLayoutMargin, GridLayoutMargin]"
  426 + :is-draggable="draggable"
  427 + :is-resizable="resizable"
  428 + :vertical-compact="true"
  429 + :use-css-transforms="true"
  430 + style="width: 100%"
  431 + >
  432 + <GridItem
  433 + v-for="item in dataBoardList"
  434 + :key="item.i"
  435 + :static="item.static"
  436 + :x="item.x"
  437 + :y="item.y"
  438 + :w="item.w"
  439 + :h="item.h"
  440 + :i="item.i"
  441 + :min-h="DEFAULT_MIN_HEIGHT"
  442 + :min-w="DEFAULT_MIN_WIDTH"
  443 + :style="{ display: 'flex', flexWrap: 'wrap' }"
  444 + class="grid-item-layout"
  445 + @resized="itemResized"
  446 + @resize="itemResize"
  447 + @moved="itemMoved"
  448 + @container-resized="itemContainerResized"
  449 + drag-ignore-from=".no-drag"
  450 + >
  451 + <WidgetWrapper
  452 + :key="item.i"
  453 + :ref="(el: Element) => setComponentRef(el, item)"
  454 + :record="item.record"
  455 + :data-source="item.record.dataSource"
  456 + >
  457 + <template #header>
  458 + <BaseWidgetHeader
  459 + :record="item.record.dataSource"
  460 + :id="item.record.id"
  461 + :panel-name="item.record.name"
  462 + @action="handleMoreAction"
  463 + >
  464 + <template #moreAction>
  465 + <Tooltip v-if="!isCustomerUser" title="趋势">
  466 + <img
  467 + :src="trendIcon"
  468 + v-if="!getIsSharePage && hasHistoryTrend(item)"
  469 + class="cursor-pointer w-4.5 h-4.5"
  470 + @click="handleOpenHistroyDataModal(item.record.dataSource)"
  471 + />
  472 + </Tooltip>
  473 + </template>
  474 + </BaseWidgetHeader>
  475 + </template>
  476 + <template #controls="{ record, add, remove, update }">
  477 + <component
  478 + :is="getComponent(item.record)"
  479 + :add="add"
  480 + :remove="remove"
  481 + :update="update"
  482 + :radio="record.radio || {}"
  483 + v-bind="getComponentConfig(item.record, record)"
  484 + :random="false"
  485 + />
  486 + </template>
  487 + </WidgetWrapper>
  488 + </GridItem>
  489 + </GridLayout>
  490 + <Empty
  491 + v-if="!dataBoardList.length"
  492 + class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
  493 + />
  494 + </Spin>
  495 + </section>
  496 + <DataBindModal
  497 + :layout="dataBoardList"
  498 + @register="register"
  499 + @update="handleUpdateComponent"
  500 + @create="getDataBoardComponent"
  501 + />
  502 + <HistoryTrendModal @register="registerHistoryDataModal" />
  503 + </section>
  504 +</template>
  505 +
  506 +<style lang="less" scoped>
  507 + .vue-grid-item:not(.vue-grid-placeholder) {
  508 + background: #fff;
  509 + border: none !important;
  510 +
  511 + /* border: 1px solid black; */
  512 + }
  513 +
  514 + .vue-grid-item .resizing {
  515 + opacity: 0.9;
  516 + }
  517 +
  518 + .vue-grid-item .static {
  519 + background: #cce;
  520 + }
  521 +
  522 + .vue-grid-item .text {
  523 + font-size: 24px;
  524 + text-align: center;
  525 + position: absolute;
  526 + top: 0;
  527 + bottom: 0;
  528 + left: 0;
  529 + right: 0;
  530 + margin: auto;
  531 + height: 100%;
  532 + width: 100%;
  533 + }
  534 +
  535 + .vue-grid-item .no-drag {
  536 + height: 100%;
  537 + width: 100%;
  538 + }
  539 +
  540 + .vue-grid-item .minMax {
  541 + font-size: 12px;
  542 + }
  543 +
  544 + .vue-grid-item .add {
  545 + cursor: pointer;
  546 + }
  547 +
  548 + .vue-draggable-handle {
  549 + position: absolute;
  550 + width: 20px;
  551 + height: 20px;
  552 + top: 0;
  553 + left: 0;
  554 + background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><circle cx='5' cy='5' r='5' fill='#999999'/></svg>")
  555 + no-repeat;
  556 + background-position: bottom right;
  557 + padding: 0 8px 8px 0;
  558 + background-repeat: no-repeat;
  559 + background-origin: content-box;
  560 + box-sizing: border-box;
  561 + cursor: pointer;
  562 + }
  563 +
  564 + .grid-item-layout {
  565 + overflow: hidden;
  566 + border: 1px solid #eee !important;
  567 + background-color: #fcfcfc !important;
  568 + }
  569 +
  570 + .board-detail:deep(.ant-page-header) {
  571 + padding: 20px 20px 0 20px;
  572 + }
  573 +
  574 + .board-detail:deep(.ant-page-header-heading) {
  575 + height: 78px;
  576 + padding: 0 20px 0 20px;
  577 + box-sizing: border-box;
  578 + background-color: #fff;
  579 + }
  580 +
  581 + [data-theme='dark'] .board-detail:deep(.ant-page-header-heading) {
  582 + @apply bg-dark-900;
  583 + }
  584 +
  585 + .board-detail:deep(.ant-page-header-heading-extra) {
  586 + margin: 0;
  587 + line-height: 78px;
  588 + }
  589 +
  590 + .board-detail:deep(.ant-page-header-content) {
  591 + padding-top: 20px;
  592 + }
  593 +
  594 + :deep(.vue-resizable-handle) {
  595 + z-index: 99;
  596 + }
  597 +</style>
... ...
  1 +export { default as BasicDataSource } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { CreateComponentType } from '../../packages/index.type';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import { dataSourceSchema } from '/@/views/visual/board/detail/config/basicConfiguration';
  5 + import {
  6 + PublicComponentValueType,
  7 + PublicFormInstaceType,
  8 + } from '/@/views/visual/dataSourceBindPanel/index.type';
  9 +
  10 + const props = defineProps<{
  11 + values: PublicComponentValueType;
  12 + componentConfig: CreateComponentType;
  13 + }>();
  14 +
  15 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  16 + labelWidth: 0,
  17 + showActionButtonGroup: false,
  18 + layout: 'horizontal',
  19 + labelCol: { span: 0 },
  20 + schemas: dataSourceSchema(false, props.componentConfig.componentConfig.key),
  21 + });
  22 +
  23 + const getFormValues = () => {
  24 + return getFieldsValue();
  25 + };
  26 +
  27 + const setFormValues = (record: Recordable) => {
  28 + return setFieldsValue(record);
  29 + };
  30 +
  31 + defineExpose({
  32 + getFormValues,
  33 + setFormValues,
  34 + validate,
  35 + resetFormValues: resetFields,
  36 + } as PublicFormInstaceType);
  37 +</script>
  38 +
  39 +<template>
  40 + <BasicForm @register="register" />
  41 +</template>
... ...
  1 +export { default as DeviceName } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentPropsConfigType } from '../../packages/index.type';
  3 +
  4 + defineProps<{
  5 + config: ComponentPropsConfigType;
  6 + }>();
  7 +</script>
  8 +
  9 +<template>
  10 + <div v-if="config.option?.componentInfo?.showDeviceName" class="h-8 font-semibold">
  11 + {{ config.option.deviceRename || config.option.deviceName }}
  12 + </div>
  13 +</template>
... ...
  1 +export { default as UpdateTime } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { Tooltip } from 'ant-design-vue';
  3 + import { formatToDateTime } from '/@/utils/dateUtil';
  4 + const props = defineProps<{
  5 + time?: number | null;
  6 + }>();
  7 +
  8 + const formatDate = (time?: Nullable<number>) => {
  9 + return props.time ? formatToDateTime(time, 'YYYY-MM-DD HH:mm:ss') : '暂无更新时间';
  10 + };
  11 +</script>
  12 +
  13 +<template>
  14 + <div class="flex p-5 justify-center items-center text-gray-400 text-xs w-full dark:text-light-50">
  15 + <Tooltip :title="formatDate(time)">
  16 + <div class="truncate px-1">
  17 + <span>更新时间:</span>
  18 + <span class="ml-2">{{ formatDate(time) }}</span>
  19 + </div>
  20 + </Tooltip>
  21 + </div>
  22 +</template>
... ...
  1 +import { FormSchema } from '/@/components/Form';
  2 +
  3 +export type BasicInfoFormValueType = Record<BasicConfigField, string>;
  4 +
  5 +export enum BasicConfigField {
  6 + NAME = 'name',
  7 + REMARK = 'remark',
  8 +}
  9 +
  10 +export const basicSchema: FormSchema[] = [
  11 + {
  12 + field: BasicConfigField.NAME,
  13 + label: '组件名称',
  14 + component: 'Input',
  15 + componentProps: {
  16 + placeholder: '请输入组件名称',
  17 + maxLength: 32,
  18 + },
  19 + },
  20 + {
  21 + field: BasicConfigField.REMARK,
  22 + label: '组件备注',
  23 + component: 'InputTextArea',
  24 + componentProps: {
  25 + placeholder: '请输入组件备注',
  26 + maxLength: 255,
  27 + },
  28 + },
  29 +];
... ...
  1 +export { default as BasicInfoForm } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { PublicFormInstaceType } from '../../index.type';
  3 + import { BasicInfoFormValueType, basicSchema } from './config';
  4 + import { BasicForm, useForm } from '/@/components/Form';
  5 +
  6 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  7 + schemas: basicSchema,
  8 + showActionButtonGroup: false,
  9 + labelWidth: 96,
  10 + });
  11 +
  12 + const getFormValues = () => {
  13 + return getFieldsValue() as BasicInfoFormValueType;
  14 + };
  15 +
  16 + const setFormValues = (record: Partial<BasicInfoFormValueType>) => {
  17 + setFieldsValue(record);
  18 + };
  19 +
  20 + defineExpose({
  21 + getFormValues,
  22 + setFormValues,
  23 + resetFormValues: resetFields,
  24 + } as PublicFormInstaceType);
  25 +</script>
  26 +
  27 +<template>
  28 + <BasicForm @register="register" class="max-w-3/4" />
  29 +</template>
... ...
  1 +export { default as DataSourceForm } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { Spin, Tooltip } from 'ant-design-vue';
  3 + import {
  4 + CopyOutlined,
  5 + SettingOutlined,
  6 + SwapOutlined,
  7 + DeleteOutlined,
  8 + } from '@ant-design/icons-vue';
  9 + import { computed } from 'vue';
  10 + import { PublicFormInstaceType, DataSourceType, SelectedWidgetKeys } from '../../index.type';
  11 + import { fetchDatasourceComponent } from '../../../packages';
  12 + import { ConfigType, CreateComponentType } from '../../../packages/index.type';
  13 + import { ref } from 'vue';
  14 + import { unref } from 'vue';
  15 + import { watch } from 'vue';
  16 + import { nextTick } from 'vue';
  17 + import { useUpdateQueue } from './useUpdateQueue';
  18 + import { useSort } from './useSort';
  19 + import { SettingModal } from '../SettingModal';
  20 + import { useModal } from '/@/components/Modal';
  21 + import { ModalParamsType } from '/#/utils';
  22 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  23 + import { toRaw } from 'vue';
  24 + import cloneDeep from 'lodash-es/cloneDeep';
  25 + import { isBoolean } from '/@/utils/is';
  26 + import { DATA_SOURCE_LIMIT_NUMBER } from '../..';
  27 + import { useMessage } from '/@/hooks/web/useMessage';
  28 +
  29 + const props = defineProps<{
  30 + selectWidgetKeys: SelectedWidgetKeys;
  31 + dataSource: DataSourceType[];
  32 + componentConfig: CreateComponentType;
  33 + }>();
  34 +
  35 + const emit = defineEmits<{
  36 + (event: 'update:dataSource', data: DataSourceType[]): void;
  37 + }>();
  38 +
  39 + const { createMessage } = useMessage();
  40 +
  41 + const [registerModal, { openModal }] = useModal();
  42 +
  43 + const { trackUpdate, triggerUpdate } = useUpdateQueue(props);
  44 +
  45 + const spinning = ref(false);
  46 +
  47 + const dataSourceFormsEl = ref<{ uuid: string; instance: PublicFormInstaceType }[]>([]);
  48 +
  49 + const getComponent = computed(() => {
  50 + try {
  51 + const { componentKey } = props.selectWidgetKeys;
  52 + const component = fetchDatasourceComponent({ key: componentKey } as ConfigType);
  53 + return component;
  54 + } catch (error) {
  55 + return '';
  56 + }
  57 + });
  58 +
  59 + const hasSettingDesignIcon = computed(() => {
  60 + const { componetDesign } = props.componentConfig.persetOption || {};
  61 + return isBoolean(componetDesign) ? componetDesign : true;
  62 + });
  63 +
  64 + const setDataSourceFormsEl = (uuid: string, instance: PublicFormInstaceType, index) => {
  65 + const findIndex = unref(props.dataSource).findIndex((item) => item.uuid === uuid);
  66 + if (~findIndex) {
  67 + dataSourceFormsEl.value[index] = { uuid, instance };
  68 + triggerUpdate(uuid, instance);
  69 + }
  70 + };
  71 +
  72 + const getFormValueByUUID = (uuid: string): Recordable => {
  73 + const el = unref(dataSourceFormsEl).find((item) => item.uuid === uuid);
  74 + if (el && el.instance) return el.instance.getFormValues();
  75 + return {};
  76 + };
  77 +
  78 + const getFormValues = (): DataSourceType[] => {
  79 + // 过滤失效form
  80 + dataSourceFormsEl.value = unref(dataSourceFormsEl).filter((item) => item.instance);
  81 +
  82 + return unref(dataSourceFormsEl).map((item) => {
  83 + const value = item.instance?.getFormValues();
  84 + const oldValue =
  85 + props.dataSource.find((temp) => temp.uuid === item.uuid) || ({} as DataSourceType);
  86 + return {
  87 + componentInfo: toRaw(oldValue.componentInfo),
  88 + ...value,
  89 + uuid: item.uuid,
  90 + };
  91 + });
  92 + };
  93 +
  94 + const setFormValues = (value: DataSourceType[]) => {
  95 + value.forEach((item) => {
  96 + const { uuid } = item;
  97 + const el = unref(dataSourceFormsEl).find((item) => item.uuid === uuid);
  98 + trackUpdate(uuid);
  99 + if (el && el.instance) {
  100 + triggerUpdate(uuid, el.instance);
  101 + }
  102 + });
  103 + };
  104 +
  105 + const validate = async () => {
  106 + try {
  107 + for (const item of unref(dataSourceFormsEl)) {
  108 + const errors = await item.instance?.validate?.();
  109 + if (isBoolean(errors) && !errors) return { flag: false, errors };
  110 + }
  111 + return { flag: true, errors: [] };
  112 + } catch (error) {
  113 + console.error(error);
  114 + return { flag: false, errors: error };
  115 + }
  116 + };
  117 +
  118 + const resetFormValues = async () => {
  119 + dataSourceFormsEl.value = unref(dataSourceFormsEl).filter((item) => item.instance);
  120 + unref(dataSourceFormsEl).forEach((item) => {
  121 + item.instance && item.instance?.resetFormValues?.();
  122 + });
  123 + };
  124 +
  125 + const handleCopy = (record: DataSourceType) => {
  126 + if (props.dataSource.length >= DATA_SOURCE_LIMIT_NUMBER) {
  127 + createMessage.warning('绑定的数据源不能超过10条~');
  128 + return;
  129 + }
  130 +
  131 + const allValues = getFormValues();
  132 + const currentRecord = getFormValueByUUID(record.uuid);
  133 + const uuid = trackUpdate();
  134 + const raw = toRaw(record);
  135 +
  136 + emit('update:dataSource', [
  137 + ...allValues,
  138 + { componentInfo: raw.componentInfo, ...currentRecord, uuid },
  139 + ]);
  140 + };
  141 +
  142 + const handleSetting = (record: DataSourceType) => {
  143 + openModal(true, { mode: DataActionModeEnum.UPDATE, record: record } as ModalParamsType);
  144 + };
  145 +
  146 + const handleDelete = (record: DataSourceType) => {
  147 + const deleteElIndex = unref(dataSourceFormsEl).findIndex((item) => item.uuid === record.uuid);
  148 + unref(dataSourceFormsEl).splice(deleteElIndex, 1);
  149 + const raw = getFormValues();
  150 + emit('update:dataSource', raw);
  151 + };
  152 +
  153 + watch(
  154 + () => props.dataSource,
  155 + async (value) => {
  156 + if (value && value.length) {
  157 + nextTick();
  158 + setFormValues(value);
  159 + }
  160 + }
  161 + );
  162 +
  163 + const { containerEl } = useSort(emit, getFormValues);
  164 +
  165 + const handleSettingOk = (data: DataSourceType) => {
  166 + const { uuid } = data;
  167 + const _dataSource = cloneDeep(props.dataSource);
  168 +
  169 + const index = _dataSource.findIndex((item) => item.uuid === uuid);
  170 +
  171 + _dataSource[index] = { ..._dataSource[index], ...data };
  172 +
  173 + emit('update:dataSource', _dataSource);
  174 + };
  175 +
  176 + defineExpose({
  177 + getFormValues,
  178 + validate,
  179 + setFormValues,
  180 + resetFormValues,
  181 + } as PublicFormInstaceType);
  182 +</script>
  183 +
  184 +<template>
  185 + <section ref="containerEl">
  186 + <Spin :spinning="spinning">
  187 + <main v-for="(item, index) in dataSource" :key="item.uuid" class="flex">
  188 + <label class="w-24 text-right pr-2">数据源{{ index + 1 }}</label>
  189 + <component
  190 + :ref="(event) => setDataSourceFormsEl(item.uuid, event, index)"
  191 + class="flex-1 bg-light-50 dark:bg-dark-400"
  192 + :is="getComponent"
  193 + :component-config="componentConfig"
  194 + :values="item"
  195 + />
  196 + <div class="w-28 flex gap-3 ml-2">
  197 + <Tooltip title="复制">
  198 + <CopyOutlined @click="handleCopy(item)" class="cursor-pointer text-lg !leading-32px" />
  199 + </Tooltip>
  200 + <Tooltip title="设置" v-if="hasSettingDesignIcon">
  201 + <SettingOutlined
  202 + @click="handleSetting(item)"
  203 + class="cursor-pointer text-lg !leading-32px"
  204 + />
  205 + </Tooltip>
  206 + <Tooltip title="拖拽排序">
  207 + <SwapOutlined
  208 + class="cursor-pointer text-lg !leading-32px svg:transform svg:rotate-90 sort-icon"
  209 + />
  210 + </Tooltip>
  211 + <Tooltip title="删除">
  212 + <DeleteOutlined
  213 + @click="handleDelete(item)"
  214 + class="cursor-pointer text-lg !leading-32px"
  215 + />
  216 + </Tooltip>
  217 + </div>
  218 + </main>
  219 + </Spin>
  220 +
  221 + <SettingModal
  222 + @register="registerModal"
  223 + @ok="handleSettingOk"
  224 + :component-config="componentConfig"
  225 + :select-widget-keys="selectWidgetKeys"
  226 + />
  227 + </section>
  228 +</template>
... ...
  1 +import { nextTick, onMounted, ref, unref } from 'vue';
  2 +import { useSortable } from '/@/hooks/web/useSortable';
  3 +import { isNullAndUnDef } from '/@/utils/is';
  4 +import { DataSourceType } from '../../index.type';
  5 +
  6 +export const useSort = (
  7 + emit: (event: 'update:dataSource', data: DataSourceType[]) => void,
  8 + getFormValues: () => DataSourceType[]
  9 +) => {
  10 + let inited = false;
  11 + const containerEl = ref<Nullable<HTMLElement>>(null);
  12 + async function handleSort() {
  13 + if (inited) return;
  14 + await nextTick();
  15 + const container = unref(containerEl);
  16 + if (!container) return;
  17 + const element: Nullable<HTMLElement> = unref(container).querySelector('.ant-spin-container');
  18 + if (!element) return;
  19 +
  20 + const { initSortable } = useSortable(element, {
  21 + handle: '.sort-icon',
  22 + onEnd: (evt) => {
  23 + const { oldIndex, newIndex } = evt;
  24 + if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) {
  25 + return;
  26 + }
  27 + const dataSource = getFormValues();
  28 +
  29 + if (oldIndex > newIndex) {
  30 + dataSource.splice(newIndex, 0, dataSource[oldIndex]);
  31 + dataSource.splice(oldIndex + 1, 1);
  32 + } else {
  33 + dataSource.splice(newIndex + 1, 0, dataSource[oldIndex]);
  34 + dataSource.splice(oldIndex, 1);
  35 + }
  36 + emit('update:dataSource', dataSource);
  37 + },
  38 + });
  39 + initSortable();
  40 + inited = true;
  41 + }
  42 +
  43 + onMounted(() => handleSort());
  44 +
  45 + return { containerEl };
  46 +};
... ...
  1 +import { nextTick } from 'vue';
  2 +import { PublicFormInstaceType, DataSourceType } from '../../index.type';
  3 +import { buildUUID } from '/@/utils/uuid';
  4 +
  5 +export const useUpdateQueue = (props: { dataSource: DataSourceType[] }) => {
  6 + const needUpdateQueue: string[] = [];
  7 +
  8 + const triggerUpdate = (uuid: string, instance: PublicFormInstaceType) => {
  9 + const index = needUpdateQueue.findIndex((item) => item === uuid);
  10 + if (~index) {
  11 + const value = props.dataSource.find((item) => item.uuid === uuid);
  12 + nextTick(() => instance?.setFormValues(value || {}));
  13 + needUpdateQueue.splice(index, 1);
  14 + }
  15 + };
  16 +
  17 + const trackUpdate = (uuid = buildUUID()) => {
  18 + needUpdateQueue.push(uuid);
  19 + return uuid;
  20 + };
  21 +
  22 + return { trackUpdate, triggerUpdate };
  23 +};
... ...
  1 +export { default as MessageAlert } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { computed } from 'vue';
  3 + import { PackagesCategoryEnum } from '../../../packages/index.type';
  4 + import { SelectedWidgetKeys } from '../../index.type';
  5 + import { Alert } from 'ant-design-vue';
  6 +
  7 + const props = defineProps<{
  8 + selectWidgetKeys: SelectedWidgetKeys;
  9 + }>();
  10 +
  11 + const alert = {
  12 + [PackagesCategoryEnum.MAP]: [
  13 + '地图组件,需绑定两个数据源,且数据源为同一设备。第一数据源为经度,第二数据源为纬度,否则地图组件不能正常显示。',
  14 + ],
  15 + [PackagesCategoryEnum.CONTROL]: [
  16 + '控制组件数据源为TCP产品,则其控制命令下发为TCP产品 物模型=>服务,且不具备状态显示功能.',
  17 + '控制组件数据源为非TCP产品,则其控制命令下发为产品 物模型=>属性,且具备状态显示功能.',
  18 + ],
  19 + };
  20 +
  21 + const getMessage = computed(() => {
  22 + const { selectWidgetKeys } = props;
  23 + const { categoryKey } = selectWidgetKeys;
  24 + return alert[categoryKey];
  25 + });
  26 +</script>
  27 +
  28 +<template>
  29 + <Alert v-if="getMessage" type="info" show-icon>
  30 + <template #description>
  31 + <div v-for="(item, index) in getMessage" :key="index">{{ item }}</div>
  32 + </template>
  33 + </Alert>
  34 +</template>
... ...
  1 +export { default as SettingModal } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { computed } from 'vue';
  3 + import { fetchConfigComponent } from '../../../packages';
  4 + import { DataSourceType, PublicFormInstaceType, SelectedWidgetKeys } from '../../index.type';
  5 + import { BasicModal, useModalInner } from '/@/components/Modal';
  6 + import { ConfigType, CreateComponentType } from '../../../packages/index.type';
  7 + import { ref } from 'vue';
  8 + import { unref } from 'vue';
  9 + import { ModalParamsType } from '/#/utils';
  10 +
  11 + const props = defineProps<{
  12 + selectWidgetKeys: SelectedWidgetKeys;
  13 + componentConfig: CreateComponentType;
  14 + }>();
  15 +
  16 + const emit = defineEmits(['register', 'ok']);
  17 +
  18 + const settingFormEl = ref<Nullable<PublicFormInstaceType>>(null);
  19 +
  20 + const getSettingComponent = computed(() => {
  21 + try {
  22 + const { componentKey } = props.selectWidgetKeys;
  23 + const component = fetchConfigComponent({ key: componentKey } as ConfigType);
  24 + return component;
  25 + } catch (error) {
  26 + return '';
  27 + }
  28 + });
  29 +
  30 + const currentEditRecord = ref<DataSourceType>({} as DataSourceType);
  31 +
  32 + const [register, { closeModal }] = useModalInner((data: ModalParamsType<DataSourceType>) => {
  33 + const { record } = data;
  34 + currentEditRecord.value = record;
  35 + setFormValues(record.componentInfo || {});
  36 + });
  37 +
  38 + const getFormValues = () => {
  39 + return unref(settingFormEl)?.getFormValues();
  40 + };
  41 +
  42 + const setFormValues = (data: Recordable) => {
  43 + unref(settingFormEl)?.setFormValues(data || {});
  44 + };
  45 +
  46 + const handleOk = () => {
  47 + const { uuid } = unref(currentEditRecord);
  48 + emit('ok', { uuid, componentInfo: getFormValues() } as DataSourceType);
  49 + closeModal();
  50 + };
  51 +
  52 + defineExpose({
  53 + getFormValues,
  54 + setFormValues,
  55 + } as PublicFormInstaceType);
  56 +</script>
  57 +
  58 +<template>
  59 + <BasicModal @register="register" title="组件设置" @ok="handleOk">
  60 + <!-- -->
  61 + <component ref="settingFormEl" :is="getSettingComponent" />
  62 + </BasicModal>
  63 +</template>
... ...
  1 +export { default as DataSourceBindPanel } from './index.vue';
  2 +
  3 +export const DATA_SOURCE_LIMIT_NUMBER = 10;
... ...
  1 +import { ValidateErrorEntity } from 'ant-design-vue/es/form/interface';
  2 +import { DataSource } from '../palette/types';
  3 +
  4 +export interface SelectedWidgetKeys {
  5 + categoryKey: string;
  6 + componentKey: string;
  7 +}
  8 +
  9 +export interface DataSourceType {
  10 + uuid: string;
  11 + componentInfo?: Recordable;
  12 + [key: string]: any;
  13 +}
  14 +
  15 +export interface PublicComponentValueType extends DataSource {
  16 + uuid: string;
  17 + [key: string]: any;
  18 +}
  19 +
  20 +export interface PublicFormInstaceType {
  21 + getFormValues: () => Recordable;
  22 + setFormValues: (data: Recordable) => void;
  23 + resetFormValues: () => Promise<void>;
  24 + validate?: () => Promise<{ flag: boolean; errors: ValidateErrorEntity }>;
  25 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { computed, ref, watch } from 'vue';
  3 + import { BasicInfoForm } from './components/BasicInfoForm';
  4 + import { ModalParamsType } from '/#/utils';
  5 + import { BasicModal, useModalInner } from '/@/components/Modal';
  6 + import { Divider, Tabs, Button, Spin } from 'ant-design-vue';
  7 + import { DataSourceForm } from './components/DataSourceForm';
  8 + import { WidgetLibrary } from '../widgetLibrary';
  9 + import {
  10 + CreateComponentType,
  11 + PackagesCategoryEnum,
  12 + PackagesCategoryNameEnum,
  13 + } from '../packages/index.type';
  14 + import { TextComponent1Config } from '../packages/components/Text/TextComponent1';
  15 + import { DataSourceType, SelectedWidgetKeys } from './index.type';
  16 + import { buildUUID } from '/@/utils/uuid';
  17 + import { unref } from 'vue';
  18 + import { AddDataComponentParams } from '/@/api/dataBoard/model';
  19 + import { useCalcNewWidgetPosition } from '../palette/hooks/useCalcNewWidgetPosition';
  20 + import { Layout } from 'vue3-grid-layout';
  21 + import { useBoardId } from '../palette/hooks/useBoardId';
  22 + import { addDataComponent, updateDataComponent } from '/@/api/dataBoard';
  23 + import { useMessage } from '/@/hooks/web/useMessage';
  24 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  25 + import { WidgetDataType } from '../palette/hooks/useDataSource';
  26 + import { DATA_SOURCE_LIMIT_NUMBER } from '.';
  27 + import { DataSource } from '../palette/types';
  28 + import { useGetComponentConfig } from '../packages/hook/useGetComponetConfig';
  29 + import { MessageAlert } from './components/MessageAlert';
  30 + import { createSelectWidgetKeysContext, createSelectWidgetModeContext } from './useContext';
  31 +
  32 + const props = defineProps<{
  33 + layout: Layout[];
  34 + }>();
  35 +
  36 + const emit = defineEmits(['register', 'ok']);
  37 +
  38 + enum TabKeyEnum {
  39 + BASIC = 'basic',
  40 + VISUAL = 'visual',
  41 + }
  42 +
  43 + const { boardId } = useBoardId();
  44 +
  45 + const { createMessage } = useMessage();
  46 +
  47 + const loading = ref(false);
  48 +
  49 + const dataSourceFormSpinning = ref(false);
  50 +
  51 + const selectWidgetKeys = ref<SelectedWidgetKeys>({
  52 + componentKey: TextComponent1Config.key,
  53 + categoryKey: PackagesCategoryEnum.TEXT,
  54 + });
  55 +
  56 + createSelectWidgetKeysContext(selectWidgetKeys);
  57 +
  58 + const getComponentConfig = computed<CreateComponentType>(() => {
  59 + return useGetComponentConfig(unref(selectWidgetKeys).componentKey);
  60 + });
  61 +
  62 + const activeKey = ref(TabKeyEnum.BASIC);
  63 +
  64 + const genNewDataSourceItem = () => {
  65 + return {
  66 + uuid: buildUUID(),
  67 + componentInfo: unref(getComponentConfig).persetOption || {},
  68 + } as DataSourceType;
  69 + };
  70 +
  71 + const dataSource = ref<DataSourceType[]>(Array.from({ length: 1 }, () => genNewDataSourceItem()));
  72 +
  73 + const currentMode = ref<DataActionModeEnum>(DataActionModeEnum.CREATE);
  74 +
  75 + createSelectWidgetModeContext(currentMode);
  76 +
  77 + const currentRecord = ref<WidgetDataType>();
  78 +
  79 + const [registerModal, { closeModal }] = useModalInner(
  80 + (params: ModalParamsType<WidgetDataType>) => {
  81 + resetFormValues();
  82 + const { mode, record } = params;
  83 + currentMode.value = mode;
  84 + currentRecord.value = record;
  85 + if (mode === DataActionModeEnum.UPDATE) {
  86 + const config = useGetComponentConfig(record.frontId);
  87 + selectWidgetKeys.value = {
  88 + componentKey: config.componentConfig.key,
  89 + categoryKey: config.componentConfig.package,
  90 + };
  91 + setFormValues(record);
  92 + } else {
  93 + dataSource.value = [genNewDataSourceItem()];
  94 + }
  95 + }
  96 + );
  97 +
  98 + const basicInfoFromEl = ref<Nullable<InstanceType<typeof BasicInfoForm>>>(null);
  99 +
  100 + const dataSourceFormEl = ref<Nullable<InstanceType<typeof DataSourceForm>>>(null);
  101 +
  102 + const handleTabsChange = (activeKey: TabKeyEnum) => {
  103 + if (activeKey === TabKeyEnum.VISUAL) {
  104 + dataSource.value = (dataSourceFormEl.value?.getFormValues() as DataSourceType[]) || [];
  105 + }
  106 + };
  107 +
  108 + const handleNewRecord = () => {
  109 + if (unref(dataSource).length >= DATA_SOURCE_LIMIT_NUMBER) {
  110 + createMessage.warning('绑定的数据源不能超过10条~');
  111 + return;
  112 + }
  113 + dataSource.value.push(genNewDataSourceItem());
  114 + };
  115 +
  116 + /**
  117 + * @description 可视化组件变化 数据源组件变更 重新赋值表单
  118 + */
  119 + watch(
  120 + () => selectWidgetKeys.value.componentKey,
  121 + (value) => {
  122 + if (value) {
  123 + dataSource.value = unref(dataSource).map((item) => ({
  124 + ...item,
  125 + componentInfo: { ...unref(getComponentConfig).persetOption, ...item.componentInfo },
  126 + }));
  127 + if (window.requestIdleCallback as unknown as boolean) {
  128 + requestIdleCallback(
  129 + () => {
  130 + setFormValues({ dataSource: unref(dataSource) } as WidgetDataType);
  131 + },
  132 + { timeout: 500 }
  133 + );
  134 + } else {
  135 + setTimeout(() => {
  136 + setFormValues({ dataSource: unref(dataSource) } as WidgetDataType);
  137 + }, 500);
  138 + }
  139 + }
  140 + }
  141 + );
  142 +
  143 + const validate = async () => {
  144 + return await unref(dataSourceFormEl)?.validate?.();
  145 + };
  146 +
  147 + const resetFormValues = () => {
  148 + unref(basicInfoFromEl)?.resetFormValues();
  149 + unref(dataSourceFormEl)?.resetFormValues();
  150 + };
  151 +
  152 + const setFormValues = (data: WidgetDataType) => {
  153 + const { dataSource: newDataSource } = data;
  154 + const { name, remark } = unref(currentRecord) || {};
  155 + dataSource.value = newDataSource;
  156 + unref(basicInfoFromEl)?.setFormValues({ name, remark });
  157 + dataSourceFormSpinning.value = true;
  158 + setTimeout(() => {
  159 + unref(dataSourceFormEl)?.setFormValues(newDataSource);
  160 + dataSourceFormSpinning.value = false;
  161 + }, 500);
  162 + };
  163 +
  164 + const getFormValues = () => {
  165 + const dataSource = (
  166 + (unref(dataSourceFormEl)?.getFormValues() as unknown as DataSource[]) || []
  167 + ).map((item) => {
  168 + Reflect.deleteProperty(item, 'uuid');
  169 + return item;
  170 + });
  171 +
  172 + const basicInfo = unref(basicInfoFromEl)?.getFormValues();
  173 +
  174 + const layout = useCalcNewWidgetPosition(props.layout);
  175 +
  176 + const frontId = unref(selectWidgetKeys).componentKey;
  177 + return {
  178 + boardId: unref(boardId),
  179 + record: {
  180 + ...(unref(currentMode) === DataActionModeEnum.UPDATE
  181 + ? { id: unref(currentRecord)?.id }
  182 + : {}),
  183 + ...basicInfo,
  184 + dataSource,
  185 + layout,
  186 + frontId,
  187 + },
  188 + } as AddDataComponentParams;
  189 + };
  190 +
  191 + const getVisualConfigTitle = computed(() => {
  192 + const { categoryKey } = unref(selectWidgetKeys);
  193 + const category = PackagesCategoryNameEnum[PackagesCategoryEnum[categoryKey]];
  194 + const { componentConfig } = unref(getComponentConfig);
  195 + return `${category} / ${componentConfig.title}`;
  196 + });
  197 +
  198 + const handleSubmit = async () => {
  199 + const validateResult = await validate();
  200 + if (validateResult && !validateResult.flag) {
  201 + const { errors } = validateResult;
  202 + if (errors && errors.errorFields.length) {
  203 + const errorRecord = errors.errorFields[0];
  204 + createMessage.warning(errorRecord.errors.join(''));
  205 + if (activeKey.value === TabKeyEnum.VISUAL) {
  206 + activeKey.value = TabKeyEnum.BASIC;
  207 + }
  208 + return;
  209 + }
  210 + }
  211 + const value = getFormValues();
  212 + try {
  213 + loading.value = true;
  214 + unref(currentMode) === DataActionModeEnum.UPDATE
  215 + ? await updateDataComponent(value)
  216 + : await addDataComponent(value);
  217 + createMessage.success(
  218 + `${unref(currentMode) === DataActionModeEnum.UPDATE ? '编辑' : '新增'}成功~`
  219 + );
  220 + closeModal();
  221 + emit('ok');
  222 + } catch (error) {
  223 + throw error;
  224 + } finally {
  225 + loading.value = false;
  226 + }
  227 + };
  228 +</script>
  229 +
  230 +<template>
  231 + <BasicModal
  232 + title="自定义组件"
  233 + width="70%"
  234 + @register="registerModal"
  235 + @ok="handleSubmit"
  236 + :ok-button-props="{ loading }"
  237 + >
  238 + <Tabs v-model:active-key="activeKey" type="card" @change="handleTabsChange" :animated="true">
  239 + <Tabs.TabPane tab="基础配置" :key="TabKeyEnum.BASIC">
  240 + <Divider orientation="left">基础信息</Divider>
  241 +
  242 + <BasicInfoForm ref="basicInfoFromEl" />
  243 +
  244 + <MessageAlert :select-widget-keys="selectWidgetKeys" />
  245 +
  246 + <Divider orientation="left">数据源配置</Divider>
  247 +
  248 + <Spin :spinning="dataSourceFormSpinning">
  249 + <DataSourceForm
  250 + ref="dataSourceFormEl"
  251 + :key="getComponentConfig.componentConfig.datasourceConKey"
  252 + :select-widget-keys="selectWidgetKeys"
  253 + v-model:dataSource="dataSource"
  254 + :component-config="getComponentConfig"
  255 + />
  256 + </Spin>
  257 +
  258 + <div class="flex justify-center">
  259 + <Button type="primary" @click="handleNewRecord">添加数据源</Button>
  260 + </div>
  261 + </Tabs.TabPane>
  262 + <Tabs.TabPane :key="TabKeyEnum.VISUAL">
  263 + <template #tab>
  264 + <span>可视化配置</span>
  265 + <span class="mx-1">-</span>
  266 + <span> {{ getVisualConfigTitle }}</span>
  267 + </template>
  268 + <WidgetLibrary v-model:checked="selectWidgetKeys" />
  269 + </Tabs.TabPane>
  270 + </Tabs>
  271 + </BasicModal>
  272 +</template>
... ...
  1 +import { Ref, inject, provide } from 'vue';
  2 +import { SelectedWidgetKeys } from './index.type';
  3 +import { DataActionModeEnum } from '/@/enums/toolEnum';
  4 +
  5 +const selectWidgetKeysKey = Symbol('select-widget-keys');
  6 +
  7 +export const createSelectWidgetKeysContext = (state: Ref<SelectedWidgetKeys>) => {
  8 + provide(selectWidgetKeysKey, state);
  9 +};
  10 +
  11 +export const useSelectWidgetKeys = () => {
  12 + return inject(selectWidgetKeysKey) as Ref<SelectedWidgetKeys>;
  13 +};
  14 +
  15 +const selectWidgetModeKey = Symbol('select-widget-mode');
  16 +
  17 +export const createSelectWidgetModeContext = (mode: Ref<DataActionModeEnum>) => {
  18 + provide(selectWidgetModeKey, mode);
  19 +};
  20 +
  21 +export const useSelectWidgetMode = () => {
  22 + return inject(selectWidgetModeKey) as Ref<DataActionModeEnum>;
  23 +};
... ...
  1 +import { tryOnUnmounted } from '@vueuse/core';
  2 +import { Component } from 'vue';
  3 +
  4 +/**
  5 + * @description 转换前端组件key, 兼容旧数据
  6 + * @param string
  7 + */
  8 +export const transformComponentKey = (string: string) => {
  9 + const CONNECTION_SYMBOL = '-';
  10 + const needTransformFlag = string.includes(CONNECTION_SYMBOL);
  11 +
  12 + if (needTransformFlag) {
  13 + return string
  14 + .split(CONNECTION_SYMBOL)
  15 + .map((item) => `${item.substring(0, 1).toUpperCase()}${item.substring(1).toLowerCase()}`)
  16 + .join('');
  17 + }
  18 +
  19 + return string;
  20 +};
  21 +
  22 +export const componentMap = new Map();
  23 +
  24 +export const registerComponent = (name: string, component: Component) => {
  25 + const _name = transformComponentKey(name);
  26 + if (componentMap.has(_name)) {
  27 + return componentMap.get(_name);
  28 + }
  29 +
  30 + componentMap.set(_name, component);
  31 +
  32 + tryOnUnmounted(() => {
  33 + uninstallComponent(_name);
  34 + });
  35 +
  36 + return component;
  37 +};
  38 +
  39 +export const uninstallComponent = (name: string) => {
  40 + componentMap.delete(transformComponentKey(name));
  41 +};
  42 +
  43 +export const getComponent = (frontId: string) => {
  44 + return componentMap.get(transformComponentKey(frontId));
  45 +};
... ...
  1 +import cloneDeep from 'lodash-es/cloneDeep';
  2 +import { ControlComponentSlidingSwitchConfig } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '/@/views/visual/packages/index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig';
  10 +
  11 +export const option: PublicPresetOptions = {
  12 + componetDesign: false,
  13 +};
  14 +
  15 +export default class Config extends PublicConfigClass implements CreateComponentType {
  16 + public key: string = ControlComponentSlidingSwitchConfig.key;
  17 +
  18 + public attr = { ...componentInitConfig };
  19 +
  20 + public componentConfig: ConfigType = cloneDeep(ControlComponentSlidingSwitchConfig);
  21 +
  22 + public persetOption = cloneDeep(option);
  23 +
  24 + public option: PublicComponentOptions;
  25 +
  26 + constructor(option: PublicComponentOptions) {
  27 + super();
  28 + this.option = { ...option };
  29 + }
  30 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  3 + import { useForm, BasicForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 +
  6 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  7 + schemas: [
  8 + {
  9 + field: ComponentConfigFieldEnum.FONT_COLOR,
  10 + label: '数值字体颜色',
  11 + component: 'ColorPicker',
  12 + changeEvent: 'update:value',
  13 + componentProps: {
  14 + defaultValue: '#FD7347',
  15 + },
  16 + },
  17 + {
  18 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  19 + label: '显示设备名称',
  20 + component: 'Checkbox',
  21 + },
  22 + ],
  23 + showActionButtonGroup: false,
  24 + labelWidth: 120,
  25 + baseColProps: {
  26 + span: 12,
  27 + },
  28 + });
  29 +
  30 + const getFormValues = () => {
  31 + return getFieldsValue();
  32 + };
  33 +
  34 + const setFormValues = (data: Recordable) => {
  35 + return setFieldsValue(data);
  36 + };
  37 +
  38 + defineExpose({
  39 + getFormValues,
  40 + setFormValues,
  41 + resetFormValues: resetFields,
  42 + } as PublicFormInstaceType);
  43 +</script>
  44 +
  45 +<template>
  46 + <BasicForm @register="register" />
  47 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { CreateComponentType } from '/@/views/visual/packages/index.type';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import {
  5 + PublicComponentValueType,
  6 + PublicFormInstaceType,
  7 + } from '/@/views/visual/dataSourceBindPanel/index.type';
  8 + import {
  9 + CommonDataSourceBindValueType,
  10 + commonDataSourceSchemas,
  11 + } from '../../../config/common.config';
  12 + import { DataSource } from '/@/views/visual/palette/types';
  13 +
  14 + defineProps<{
  15 + values: PublicComponentValueType;
  16 + componentConfig: CreateComponentType;
  17 + }>();
  18 +
  19 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  20 + labelWidth: 0,
  21 + showActionButtonGroup: false,
  22 + layout: 'horizontal',
  23 + labelCol: { span: 0 },
  24 + schemas: commonDataSourceSchemas(),
  25 + });
  26 +
  27 + const getFormValues = () => {
  28 + let value = getFieldsValue() as CommonDataSourceBindValueType;
  29 + value = {
  30 + ...value,
  31 + customCommand: {
  32 + transportType: value.transportType,
  33 + service: value.service,
  34 + command: value.command,
  35 + commandType: value.commandType,
  36 + },
  37 + };
  38 + return value;
  39 + };
  40 +
  41 + const setFormValues = (record: DataSource) => {
  42 + const { customCommand } = record;
  43 + return setFieldsValue({
  44 + ...record,
  45 + transportType: customCommand?.transportType,
  46 + service: customCommand?.service,
  47 + command: customCommand?.command,
  48 + commandType: customCommand?.commandType,
  49 + } as unknown as Partial<CommonDataSourceBindValueType>);
  50 + };
  51 +
  52 + defineExpose({
  53 + getFormValues,
  54 + setFormValues,
  55 + validate,
  56 + resetFormValues: resetFields,
  57 + } as PublicFormInstaceType);
  58 +</script>
  59 +
  60 +<template>
  61 + <BasicForm @register="register" />
  62 +</template>
... ...
  1 +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys';
  2 +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type';
  3 +
  4 +const componentKeys = useComponentKeys('ControlComponentSlidingSwitch');
  5 +
  6 +export const ControlComponentSlidingSwitchConfig: ConfigType = {
  7 + ...componentKeys,
  8 + title: '控制组件2',
  9 + package: PackagesCategoryEnum.CONTROL,
  10 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type';
  3 + import { option } from './config';
  4 + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket';
  5 + import { Spin } from 'ant-design-vue';
  6 + import { ref } from 'vue';
  7 + import { useComponentScale } from '../../../hook/useComponentScale';
  8 + import { useSendCommand } from '../../../hook/useSendCommand';
  9 + const props = defineProps<{
  10 + config: ComponentPropsConfigType<typeof option>;
  11 + }>();
  12 +
  13 + const { getScale } = useComponentScale(props);
  14 +
  15 + const currentValue = ref(false);
  16 +
  17 + const { sendCommand, loading } = useSendCommand();
  18 + const handleChange = async (event: Event) => {
  19 + const target = event.target as HTMLInputElement;
  20 + const value = target.checked;
  21 +
  22 + const flag = await sendCommand(props.config.option, value);
  23 + if (flag) currentValue.value = value;
  24 + flag ? (currentValue.value = value) : (target.checked = !value);
  25 + };
  26 +
  27 + const updateFn: DataFetchUpdateFn = (message, attribute) => {
  28 + const { data = {} } = message;
  29 + const [latest] = data[attribute] || [];
  30 + const [_, value] = latest;
  31 + currentValue.value = !!value;
  32 + };
  33 +
  34 + useDataFetch(props, updateFn);
  35 +</script>
  36 +
  37 +<template>
  38 + <main class="w-full h-full flex flex-col justify-center items-center">
  39 + <Spin :spinning="loading">
  40 + <label class="sliding-switch" :style="getScale">
  41 + <input
  42 + type="checkbox"
  43 + :value="currentValue"
  44 + :checked="currentValue"
  45 + @change="handleChange"
  46 + />
  47 + <span class="slider"></span>
  48 + <span class="on">ON</span>
  49 + <span class="off">OFF</span>
  50 + </label>
  51 + <div class="text-center mt-2 text-gray-700" :style="getScale"> 属性 </div>
  52 + </Spin>
  53 + </main>
  54 +</template>
  55 +
  56 +<style scoped lang="less">
  57 + :deep(.ant-spin-container) {
  58 + @apply !flex !flex-col justify-center items-center !flex-nowrap;
  59 + }
  60 +
  61 + .sliding-switch {
  62 + position: relative;
  63 + display: block;
  64 + font-weight: 700;
  65 + line-height: 40px;
  66 + width: 80px;
  67 + height: 40px;
  68 + font-size: 14px;
  69 + cursor: pointer;
  70 + user-select: none;
  71 +
  72 + input[type='checkbox'] {
  73 + display: none;
  74 + }
  75 +
  76 + .slider {
  77 + width: 80px;
  78 + height: 40px;
  79 + display: flex;
  80 + align-items: center;
  81 + box-sizing: border-box;
  82 + border: 2px solid #ecf0f3;
  83 + border-radius: 20px;
  84 + box-shadow: -2px -2px 8px #fff, -2px -2px 12px hsl(0deg 0% 100% / 50%),
  85 + inset -2px -2px 8px #fff, inset -2px -2px 12px hsl(0deg 0% 100% / 50%),
  86 + inset 2px 2px 4px hsl(0deg 0% 100% / 10%), inset 2px 2px 8px rgb(0 0 0 / 30%),
  87 + 2px 2px 8px rgb(0 0 0 / 30%);
  88 + background-color: #ecf0f3;
  89 + z-index: -1;
  90 + }
  91 +
  92 + .slider::after {
  93 + cursor: pointer;
  94 + display: block;
  95 + content: '';
  96 + width: 24px;
  97 + height: 24px;
  98 + border-radius: 50%;
  99 + margin-left: 6px;
  100 + margin-right: 6px;
  101 + background-color: #ecf0f3;
  102 + box-shadow: -2px -2px 8px #fff, -2px -2px 12px hsl(0deg 0% 100% / 50%),
  103 + inset 2px 2px 4px hsl(0deg 0% 100% / 10%), 2px 2px 8px rgb(0 0 0 / 30%);
  104 + z-index: 999;
  105 + transition: 0.5s;
  106 + }
  107 +
  108 + input:checked ~ .off {
  109 + opacity: 0;
  110 + }
  111 +
  112 + input:checked ~ .slider::after {
  113 + transform: translateX(35px);
  114 + }
  115 +
  116 + input:not(:checked) ~ .on {
  117 + opacity: 0;
  118 + transform: translateX(0);
  119 + }
  120 +
  121 + .on,
  122 + .off {
  123 + position: absolute;
  124 + top: 0;
  125 + display: inline-block;
  126 + margin-left: 3px;
  127 + width: 34px;
  128 + text-align: center;
  129 + transition: 0.2s;
  130 + }
  131 +
  132 + .on {
  133 + color: #039be5;
  134 + }
  135 +
  136 + .off {
  137 + right: 6px;
  138 + color: #999;
  139 + }
  140 + }
  141 +</style>
... ...
  1 +import cloneDeep from 'lodash-es/cloneDeep';
  2 +import { ControlComponentSwitchWithIconConfig } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '/@/views/visual/packages/index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig';
  10 +import { ComponentConfigFieldEnum } from '../../../enum';
  11 +
  12 +export const option: PublicPresetOptions = {
  13 + [ComponentConfigFieldEnum.ICON]: 'shuiwen',
  14 + [ComponentConfigFieldEnum.ICON_COLOR]: '#377DFF',
  15 + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false,
  16 +};
  17 +
  18 +export default class Config extends PublicConfigClass implements CreateComponentType {
  19 + public key: string = ControlComponentSwitchWithIconConfig.key;
  20 +
  21 + public attr = { ...componentInitConfig };
  22 +
  23 + public componentConfig: ConfigType = cloneDeep(ControlComponentSwitchWithIconConfig);
  24 +
  25 + public persetOption = cloneDeep(option);
  26 +
  27 + public option: PublicComponentOptions;
  28 +
  29 + constructor(option: PublicComponentOptions) {
  30 + super();
  31 + this.option = { ...option };
  32 + }
  33 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  3 + import { useForm, BasicForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 + import { option } from './config';
  6 +
  7 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  8 + schemas: [
  9 + {
  10 + field: ComponentConfigFieldEnum.ICON_COLOR,
  11 + label: '图标颜色',
  12 + component: 'ColorPicker',
  13 + changeEvent: 'update:value',
  14 + defaultValue: option.iconColor,
  15 + },
  16 + {
  17 + field: ComponentConfigFieldEnum.ICON,
  18 + label: '图标',
  19 + component: 'IconDrawer',
  20 + changeEvent: 'update:value',
  21 + defaultValue: option.icon,
  22 + componentProps({ formModel }) {
  23 + const color = formModel[ComponentConfigFieldEnum.ICON_COLOR];
  24 + return {
  25 + color,
  26 + };
  27 + },
  28 + },
  29 + {
  30 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  31 + label: '显示设备名称',
  32 + component: 'Checkbox',
  33 + defaultValue: option.showDeviceName,
  34 + },
  35 + ],
  36 + showActionButtonGroup: false,
  37 + labelWidth: 120,
  38 + baseColProps: {
  39 + span: 12,
  40 + },
  41 + });
  42 +
  43 + const getFormValues = () => {
  44 + return getFieldsValue();
  45 + };
  46 +
  47 + const setFormValues = (data: Recordable) => {
  48 + return setFieldsValue(data);
  49 + };
  50 +
  51 + defineExpose({
  52 + getFormValues,
  53 + setFormValues,
  54 + resetFormValues: resetFields,
  55 + } as PublicFormInstaceType);
  56 +</script>
  57 +
  58 +<template>
  59 + <BasicForm @register="register" />
  60 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { CreateComponentType } from '/@/views/visual/packages/index.type';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import {
  5 + PublicComponentValueType,
  6 + PublicFormInstaceType,
  7 + } from '/@/views/visual/dataSourceBindPanel/index.type';
  8 + import {
  9 + CommonDataSourceBindValueType,
  10 + commonDataSourceSchemas,
  11 + } from '../../../config/common.config';
  12 + import { DataSource } from '/@/views/visual/palette/types';
  13 +
  14 + defineProps<{
  15 + values: PublicComponentValueType;
  16 + componentConfig: CreateComponentType;
  17 + }>();
  18 +
  19 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  20 + labelWidth: 0,
  21 + showActionButtonGroup: false,
  22 + layout: 'horizontal',
  23 + labelCol: { span: 0 },
  24 + schemas: commonDataSourceSchemas(),
  25 + });
  26 +
  27 + const getFormValues = () => {
  28 + let value = getFieldsValue() as CommonDataSourceBindValueType;
  29 + value = {
  30 + ...value,
  31 + customCommand: {
  32 + transportType: value.transportType,
  33 + service: value.service,
  34 + command: value.command,
  35 + commandType: value.commandType,
  36 + },
  37 + };
  38 + return value;
  39 + };
  40 +
  41 + const setFormValues = (record: DataSource) => {
  42 + const { customCommand } = record;
  43 + return setFieldsValue({
  44 + ...record,
  45 + transportType: customCommand?.transportType,
  46 + service: customCommand?.service,
  47 + command: customCommand?.command,
  48 + commandType: customCommand?.commandType,
  49 + } as unknown as Partial<CommonDataSourceBindValueType>);
  50 + };
  51 +
  52 + defineExpose({
  53 + getFormValues,
  54 + setFormValues,
  55 + validate,
  56 + resetFormValues: resetFields,
  57 + } as PublicFormInstaceType);
  58 +</script>
  59 +
  60 +<template>
  61 + <BasicForm @register="register" />
  62 +</template>
... ...
  1 +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys';
  2 +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type';
  3 +
  4 +const componentKeys = useComponentKeys('ControlComponentSwitchWithIcon');
  5 +
  6 +export const ControlComponentSwitchWithIconConfig: ConfigType = {
  7 + ...componentKeys,
  8 + title: '控制组件1',
  9 + package: PackagesCategoryEnum.CONTROL,
  10 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type';
  3 + import { option } from './config';
  4 + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket';
  5 + import { SvgIcon } from '/@/components/Icon';
  6 + import { Switch } from 'ant-design-vue';
  7 + import { computed, ref } from 'vue';
  8 + import { useComponentScale } from '../../../hook/useComponentScale';
  9 + import { useSendCommand } from '../../../hook/useSendCommand';
  10 + import { unref } from 'vue';
  11 +
  12 + const props = defineProps<{
  13 + config: ComponentPropsConfigType<typeof option>;
  14 + }>();
  15 +
  16 + const checked = ref(false);
  17 +
  18 + const getDesign = computed(() => {
  19 + const { option, persetOption } = props.config;
  20 + const { componentInfo, attribute, attributeRename } = option;
  21 + const { icon: presetIcon, iconColor: presetIconColor } = persetOption || {};
  22 + const { icon, iconColor } = componentInfo || {};
  23 + return {
  24 + icon: icon ?? presetIcon,
  25 + iconColor: iconColor || presetIconColor,
  26 + attribute: attributeRename || attribute,
  27 + };
  28 + });
  29 +
  30 + const { sendCommand, loading } = useSendCommand();
  31 + const handleChange = async () => {
  32 + const flag = await sendCommand(props.config.option, unref(checked));
  33 + if (!flag) checked.value = !unref(checked);
  34 + };
  35 +
  36 + const updateFn: DataFetchUpdateFn = (message, attribute) => {
  37 + const { data = {} } = message;
  38 + const [latest] = data[attribute] || [];
  39 + const [_, value] = latest;
  40 + checked.value = !!value;
  41 + };
  42 +
  43 + useDataFetch(props, updateFn);
  44 + const { getScale } = useComponentScale(props);
  45 +</script>
  46 +
  47 +<template>
  48 + <main class="w-full h-full flex justify-center items-center" :style="getScale">
  49 + <div class="flex flex-col justify-center items-center mr-20">
  50 + <SvgIcon
  51 + :name="getDesign.icon"
  52 + prefix="iconfont"
  53 + :style="{ color: getDesign.iconColor }"
  54 + :size="50"
  55 + />
  56 + <span class="mt-3 truncate text-gray-500 text-xs text-center">
  57 + {{ getDesign.attribute || '属性' }}
  58 + </span>
  59 + </div>
  60 + <Switch v-model:checked="checked" :loading="loading" @change="handleChange" />
  61 + </main>
  62 +</template>
... ...
  1 +import cloneDeep from 'lodash-es/cloneDeep';
  2 +import { ControlComponentToggleSwitchConfig } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '/@/views/visual/packages/index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig';
  10 +
  11 +export const option: PublicPresetOptions = {
  12 + componetDesign: false,
  13 +};
  14 +
  15 +export default class Config extends PublicConfigClass implements CreateComponentType {
  16 + public key: string = ControlComponentToggleSwitchConfig.key;
  17 +
  18 + public attr = { ...componentInitConfig };
  19 +
  20 + public componentConfig: ConfigType = cloneDeep(ControlComponentToggleSwitchConfig);
  21 +
  22 + public persetOption = cloneDeep(option);
  23 +
  24 + public option: PublicComponentOptions;
  25 +
  26 + constructor(option: PublicComponentOptions) {
  27 + super();
  28 + this.option = { ...option };
  29 + }
  30 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  3 + import { useForm, BasicForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 +
  6 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  7 + schemas: [
  8 + {
  9 + field: ComponentConfigFieldEnum.FONT_COLOR,
  10 + label: '数值字体颜色',
  11 + component: 'ColorPicker',
  12 + changeEvent: 'update:value',
  13 + componentProps: {
  14 + defaultValue: '#FD7347',
  15 + },
  16 + },
  17 + {
  18 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  19 + label: '显示设备名称',
  20 + component: 'Checkbox',
  21 + },
  22 + ],
  23 + showActionButtonGroup: false,
  24 + labelWidth: 120,
  25 + baseColProps: {
  26 + span: 12,
  27 + },
  28 + });
  29 +
  30 + const getFormValues = () => {
  31 + return getFieldsValue();
  32 + };
  33 +
  34 + const setFormValues = (data: Recordable) => {
  35 + return setFieldsValue(data);
  36 + };
  37 +
  38 + defineExpose({
  39 + getFormValues,
  40 + setFormValues,
  41 + resetFormValues: resetFields,
  42 + } as PublicFormInstaceType);
  43 +</script>
  44 +
  45 +<template>
  46 + <BasicForm @register="register" />
  47 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { CreateComponentType } from '/@/views/visual/packages/index.type';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import {
  5 + PublicComponentValueType,
  6 + PublicFormInstaceType,
  7 + } from '/@/views/visual/dataSourceBindPanel/index.type';
  8 + import {
  9 + CommonDataSourceBindValueType,
  10 + commonDataSourceSchemas,
  11 + } from '../../../config/common.config';
  12 + import { DataSource } from '/@/views/visual/palette/types';
  13 +
  14 + defineProps<{
  15 + values: PublicComponentValueType;
  16 + componentConfig: CreateComponentType;
  17 + }>();
  18 +
  19 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  20 + labelWidth: 0,
  21 + showActionButtonGroup: false,
  22 + layout: 'horizontal',
  23 + labelCol: { span: 0 },
  24 + schemas: commonDataSourceSchemas(),
  25 + });
  26 +
  27 + const getFormValues = () => {
  28 + let value = getFieldsValue() as CommonDataSourceBindValueType;
  29 + value = {
  30 + ...value,
  31 + customCommand: {
  32 + transportType: value.transportType,
  33 + service: value.service,
  34 + command: value.command,
  35 + commandType: value.commandType,
  36 + },
  37 + };
  38 + return value;
  39 + };
  40 +
  41 + const setFormValues = (record: DataSource) => {
  42 + const { customCommand } = record;
  43 + return setFieldsValue({
  44 + ...record,
  45 + transportType: customCommand?.transportType,
  46 + service: customCommand?.service,
  47 + command: customCommand?.command,
  48 + commandType: customCommand?.commandType,
  49 + } as unknown as Partial<CommonDataSourceBindValueType>);
  50 + };
  51 +
  52 + defineExpose({
  53 + getFormValues,
  54 + setFormValues,
  55 + validate,
  56 + resetFormValues: resetFields,
  57 + } as PublicFormInstaceType);
  58 +</script>
  59 +
  60 +<template>
  61 + <BasicForm @register="register" />
  62 +</template>
... ...
  1 +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys';
  2 +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type';
  3 +
  4 +const componentKeys = useComponentKeys('ControlComponentToggleSwitch');
  5 +
  6 +export const ControlComponentToggleSwitchConfig: ConfigType = {
  7 + ...componentKeys,
  8 + title: '控制组件3',
  9 + package: PackagesCategoryEnum.CONTROL,
  10 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type';
  3 + import { option } from './config';
  4 + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket';
  5 + import { Spin } from 'ant-design-vue';
  6 + import { ref } from 'vue';
  7 + import { useComponentScale } from '../../../hook/useComponentScale';
  8 + import { useSendCommand } from '../../../hook/useSendCommand';
  9 +
  10 + const props = defineProps<{
  11 + config: ComponentPropsConfigType<typeof option>;
  12 + }>();
  13 +
  14 + const { getScale } = useComponentScale(props);
  15 +
  16 + const currentValue = ref(false);
  17 +
  18 + const { loading, sendCommand } = useSendCommand();
  19 + const handleChange = async (event: Event) => {
  20 + const target = event.target as HTMLInputElement;
  21 + const value = target.checked;
  22 + const flag = await sendCommand(props.config.option, value);
  23 + flag ? (currentValue.value = value) : (target.checked = !value);
  24 + };
  25 +
  26 + const updateFn: DataFetchUpdateFn = (message, attribute) => {
  27 + const { data = {} } = message;
  28 + const [latest] = data[attribute] || [];
  29 + const [_, value] = latest;
  30 + currentValue.value = !!value;
  31 + };
  32 +
  33 + useDataFetch(props, updateFn);
  34 +</script>
  35 +
  36 +<template>
  37 + <main class="w-full h-full flex flex-col justify-center items-center">
  38 + <Spin :spinning="loading" class="w-full h-full">
  39 + <div class="toggle-switch" :style="getScale">
  40 + <label class="switch">
  41 + <input type="checkbox" :checked="currentValue" @change="handleChange" />
  42 + <div class="button">
  43 + <div class="light"></div>
  44 + <div class="dots"></div>
  45 + <div class="characters"></div>
  46 + <div class="shine"></div>
  47 + <div class="shadow"></div>
  48 + </div>
  49 + </label>
  50 + </div>
  51 + <div class="text-center mt-2 text-gray-500" :style="getScale">属性</div>
  52 + </Spin>
  53 + </main>
  54 +</template>
  55 +
  56 +<style scoped>
  57 + :deep(.ant-spin-container) {
  58 + @apply !flex !flex-col justify-center items-center;
  59 + }
  60 +
  61 + .toggle-switch {
  62 + /* flex: 1 1 auto; */
  63 + max-width: 75px;
  64 + width: 75px;
  65 + max-height: 100px;
  66 + height: 100px;
  67 +
  68 + /* height: 97.5px; */
  69 + display: flex;
  70 + }
  71 +
  72 + .switch {
  73 + background-color: black;
  74 + box-sizing: border-box;
  75 + width: 100%;
  76 + height: 100%;
  77 + box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.2), 0 0 1px 2px black, inset 0 2px 2px -2px white,
  78 + inset 0 0 2px 15px #47434c, inset 0 0 2px 22px black;
  79 + border-radius: 5px;
  80 + padding: 10px;
  81 + perspective: 700px;
  82 + }
  83 +
  84 + .switch input {
  85 + display: none;
  86 + }
  87 +
  88 + .switch input:checked + .button {
  89 + transform: translateZ(20px) rotateX(25deg);
  90 + box-shadow: 0 -5px 10px #ff1818;
  91 + }
  92 +
  93 + .switch input:checked + .button .light {
  94 + animation: flicker 0.2s infinite 0.3s;
  95 + }
  96 +
  97 + .switch input:checked + .button .shine {
  98 + opacity: 1;
  99 + }
  100 +
  101 + .switch input:checked + .button .shadow {
  102 + opacity: 0;
  103 + }
  104 +
  105 + .switch .button {
  106 + display: flex;
  107 + justify-content: center;
  108 + align-items: center;
  109 + transition: all 0.3s cubic-bezier(1, 0, 1, 1);
  110 + transform-origin: center center -20px;
  111 + transform: translateZ(20px) rotateX(-25deg);
  112 + transform-style: preserve-3d;
  113 + width: 100%;
  114 + height: 100%;
  115 + position: relative;
  116 + cursor: pointer;
  117 + background: linear-gradient(#980000 0%, #6f0000 30%, #6f0000 70%, #980000 100%);
  118 + background-color: #9b0621;
  119 + background-repeat: no-repeat;
  120 + }
  121 +
  122 + .switch .button::before {
  123 + content: '';
  124 + background: linear-gradient(
  125 + rgba(255, 255, 255, 0.8) 10%,
  126 + rgba(255, 255, 255, 0.3) 30%,
  127 + #650000 75%,
  128 + #320000
  129 + )
  130 + 50% 50%/97% 97%,
  131 + #b10000;
  132 + background-repeat: no-repeat;
  133 + width: 100%;
  134 + height: 30px;
  135 + transform-origin: top;
  136 + transform: rotateX(-90deg);
  137 + position: absolute;
  138 + top: 0;
  139 + }
  140 +
  141 + .switch .button::after {
  142 + content: '';
  143 + background-image: linear-gradient(#650000, #320000);
  144 + width: 100%;
  145 + height: 30px;
  146 + transform-origin: top;
  147 + transform: translateY(30px) rotateX(-90deg);
  148 + position: absolute;
  149 + bottom: 0;
  150 + box-shadow: 0 30px 8px 0 black, 0 60px 20px 0 rgb(0 0 0 / 50%);
  151 + }
  152 +
  153 + .switch .light {
  154 + opacity: 0;
  155 + animation: light-off 1s;
  156 + position: absolute;
  157 + width: 80%;
  158 + height: 80%;
  159 + background-image: radial-gradient(#ffc97e, transparent 40%),
  160 + radial-gradient(circle, #ff1818 50%, transparent 80%);
  161 + }
  162 +
  163 + .switch .dots {
  164 + position: absolute;
  165 + width: 100%;
  166 + height: 100%;
  167 + background-image: radial-gradient(transparent 30%, rgba(101, 0, 0, 0.7) 70%);
  168 + background-size: 10px 10px;
  169 + }
  170 +
  171 + .switch .characters {
  172 + position: absolute;
  173 + width: 100%;
  174 + height: 100%;
  175 + background: linear-gradient(white, white) 50% 20%/5% 20%,
  176 + radial-gradient(circle, transparent 50%, white 52%, white 70%, transparent 72%) 50% 80%/33%
  177 + 25%;
  178 + background-repeat: no-repeat;
  179 + }
  180 +
  181 + .switch .shine {
  182 + transition: all 0.3s cubic-bezier(1, 0, 1, 1);
  183 + opacity: 0.3;
  184 + position: absolute;
  185 + width: 100%;
  186 + height: 100%;
  187 + background: linear-gradient(white, transparent 3%) 50% 50%/97% 97%,
  188 + linear-gradient(
  189 + rgba(255, 255, 255, 0.5),
  190 + transparent 50%,
  191 + transparent 80%,
  192 + rgba(255, 255, 255, 0.5)
  193 + )
  194 + 50% 50%/97% 97%;
  195 + background-repeat: no-repeat;
  196 + }
  197 +
  198 + .switch .shadow {
  199 + transition: all 0.3s cubic-bezier(1, 0, 1, 1);
  200 + opacity: 1;
  201 + position: absolute;
  202 + width: 100%;
  203 + height: 100%;
  204 + background: linear-gradient(transparent 70%, rgba(0, 0, 0, 0.8));
  205 + background-repeat: no-repeat;
  206 + }
  207 +
  208 + @keyframes flicker {
  209 + 0% {
  210 + opacity: 1;
  211 + }
  212 +
  213 + 80% {
  214 + opacity: 0.8;
  215 + }
  216 +
  217 + 100% {
  218 + opacity: 1;
  219 + }
  220 + }
  221 +
  222 + @keyframes light-off {
  223 + 0% {
  224 + opacity: 1;
  225 + }
  226 +
  227 + 80% {
  228 + opacity: 0;
  229 + }
  230 + }
  231 +</style>
... ...
  1 +import { ControlComponentSlidingSwitchConfig } from './ControlComponentSlidingSwitch';
  2 +import { ControlComponentSwitchWithIconConfig } from './ControlComponentSwitchWithIcon';
  3 +import { ControlComponentToggleSwitchConfig } from './ControlComponentToggleSwitch';
  4 +
  5 +export const ControlList = [
  6 + ControlComponentSwitchWithIconConfig,
  7 + ControlComponentSlidingSwitchConfig,
  8 + ControlComponentToggleSwitchConfig,
  9 +];
... ...
  1 +import cloneDeep from 'lodash-es/cloneDeep';
  2 +import { RectFlowmeteConfig } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '/@/views/visual/packages/index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig';
  10 +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  11 +
  12 +export const option: PublicPresetOptions = {
  13 + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false,
  14 + [ComponentConfigFieldEnum.UNIT]: 'm',
  15 + [ComponentConfigFieldEnum.FLOWMETER_CONFIG]: {
  16 + [ComponentConfigFieldEnum.BACKGROUND_COLOR]: '#8badcb',
  17 + [ComponentConfigFieldEnum.WAVE_FIRST]: '#4579e2',
  18 + [ComponentConfigFieldEnum.WAVE_SECOND]: '#3461c1',
  19 + [ComponentConfigFieldEnum.WAVE_THIRD]: '#2d55aa',
  20 + },
  21 +};
  22 +
  23 +export default class Config extends PublicConfigClass implements CreateComponentType {
  24 + public key: string = RectFlowmeteConfig.key;
  25 +
  26 + public attr = { ...componentInitConfig };
  27 +
  28 + public componentConfig: ConfigType = cloneDeep(RectFlowmeteConfig);
  29 +
  30 + public persetOption = cloneDeep(option);
  31 +
  32 + public option: PublicComponentOptions;
  33 +
  34 + constructor(option: PublicComponentOptions) {
  35 + super();
  36 + this.option = { ...option };
  37 + }
  38 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  3 + import { useForm, BasicForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 + import { option } from './config';
  6 +
  7 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  8 + schemas: [
  9 + {
  10 + field: ComponentConfigFieldEnum.BACKGROUND_COLOR,
  11 + label: '背景颜色',
  12 + component: 'ColorPicker',
  13 + changeEvent: 'update:value',
  14 + defaultValue: option.flowmeterConfig?.backgroundColor,
  15 + },
  16 + {
  17 + field: ComponentConfigFieldEnum.WAVE_FIRST,
  18 + label: '浪1',
  19 + component: 'ColorPicker',
  20 + changeEvent: 'update:value',
  21 + defaultValue: option.flowmeterConfig?.waveFirst,
  22 + },
  23 + {
  24 + field: ComponentConfigFieldEnum.WAVE_SECOND,
  25 + label: '浪2',
  26 + component: 'ColorPicker',
  27 + changeEvent: 'update:value',
  28 + defaultValue: option.flowmeterConfig?.waveSecond,
  29 + },
  30 + {
  31 + field: ComponentConfigFieldEnum.WAVE_THIRD,
  32 + label: '浪3',
  33 + component: 'ColorPicker',
  34 + changeEvent: 'update:value',
  35 + defaultValue: option.flowmeterConfig?.waveThird,
  36 + },
  37 + {
  38 + field: ComponentConfigFieldEnum.UNIT,
  39 + label: '单位',
  40 + component: 'Input',
  41 + defaultValue: option.unit,
  42 + },
  43 + ],
  44 + showActionButtonGroup: false,
  45 + labelWidth: 120,
  46 + baseColProps: {
  47 + span: 12,
  48 + },
  49 + });
  50 +
  51 + const getFormValues = () => {
  52 + return getFieldsValue();
  53 + };
  54 +
  55 + const setFormValues = (data: Recordable) => {
  56 + return setFieldsValue(data);
  57 + };
  58 +
  59 + defineExpose({
  60 + getFormValues,
  61 + setFormValues,
  62 + resetFormValues: resetFields,
  63 + } as PublicFormInstaceType);
  64 +</script>
  65 +
  66 +<template>
  67 + <BasicForm @register="register" />
  68 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { CreateComponentType } from '/@/views/visual/packages/index.type';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import { dataSourceSchema } from '/@/views/visual/board/detail/config/basicConfiguration';
  5 + import {
  6 + PublicComponentValueType,
  7 + PublicFormInstaceType,
  8 + } from '/@/views/visual/dataSourceBindPanel/index.type';
  9 +
  10 + const props = defineProps<{
  11 + values: PublicComponentValueType;
  12 + componentConfig: CreateComponentType;
  13 + }>();
  14 +
  15 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  16 + labelWidth: 0,
  17 + showActionButtonGroup: false,
  18 + layout: 'horizontal',
  19 + labelCol: { span: 0 },
  20 + schemas: dataSourceSchema(false, props.componentConfig.componentConfig.key),
  21 + });
  22 +
  23 + const getFormValues = () => {
  24 + return getFieldsValue();
  25 + };
  26 +
  27 + const setFormValues = (record: Recordable) => {
  28 + return setFieldsValue(record);
  29 + };
  30 +
  31 + defineExpose({
  32 + getFormValues,
  33 + setFormValues,
  34 + validate,
  35 + resetFormValues: resetFields,
  36 + } as PublicFormInstaceType);
  37 +</script>
  38 +
  39 +<template>
  40 + <BasicForm @register="register" />
  41 +</template>
... ...
  1 +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys';
  2 +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type';
  3 +
  4 +const componentKeys = useComponentKeys('RectFlowmeter');
  5 +
  6 +export const RectFlowmeteConfig: ConfigType = {
  7 + ...componentKeys,
  8 + title: '流量计1',
  9 + package: PackagesCategoryEnum.FLOWMETER,
  10 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type';
  3 + import { option } from './config';
  4 + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket';
  5 + import { computed } from 'vue';
  6 + import { ref } from 'vue';
  7 + import { unref } from 'vue';
  8 +
  9 + const props = defineProps<{
  10 + config: ComponentPropsConfigType<typeof option>;
  11 + }>();
  12 +
  13 + const currentValue = ref(0);
  14 +
  15 + const getDesign = computed(() => {
  16 + const { option, persetOption } = props.config;
  17 + const { componentInfo } = option;
  18 + const { flowmeterConfig, unit } = componentInfo;
  19 + const { backgroundColor, waveFirst, waveSecond, waveThird } = flowmeterConfig || {};
  20 + const { flowmeterConfig: presetFlowmeterConfig, unit: persetUnit } = persetOption || {};
  21 + const {
  22 + backgroundColor: presetBackgroundColor,
  23 + waveFirst: presetWaveFirst,
  24 + waveSecond: presetWaveSecond,
  25 + waveThird: presetWaveThird,
  26 + } = presetFlowmeterConfig || {};
  27 + return {
  28 + backgroundColor: backgroundColor ?? presetBackgroundColor,
  29 + waveFirst: waveFirst ?? presetWaveFirst,
  30 + waveSecond: waveSecond ?? presetWaveSecond,
  31 + waveThird: waveThird ?? presetWaveThird,
  32 + unit: unit ?? persetUnit,
  33 + };
  34 + });
  35 +
  36 + const getWaveHeight = computed(() => {
  37 + const value = unref(currentValue);
  38 + return value <= 0 ? 0 : -value - 15;
  39 + });
  40 +
  41 + const getHeight = computed(() => {
  42 + const value = unref(currentValue);
  43 + return value >= 100 ? 0 : 100 - value + 10;
  44 + });
  45 +
  46 + const updateFn: DataFetchUpdateFn = (message, attribute) => {
  47 + const { data = {} } = message;
  48 + const [latest] = data[attribute] || [];
  49 + const [_, value] = latest;
  50 + currentValue.value = Number(value);
  51 + };
  52 +
  53 + useDataFetch(props, updateFn);
  54 +</script>
  55 +
  56 +<template>
  57 + <main class="w-full h-full flex flex-col justify-center items-center">
  58 + <svg
  59 + class="waves-rect"
  60 + viewBox="0 0 100 100"
  61 + preserveAspectRatio="none"
  62 + xmlns="http://www.w3.org/2000/svg"
  63 + xmlns:xlink="http://www.w3.org/1999/xlink"
  64 + >
  65 + <defs>
  66 + <path
  67 + id="wave"
  68 + d="M-160 118c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v100h-352z"
  69 + />
  70 + </defs>
  71 + <rect
  72 + class="bgColor"
  73 + x="0"
  74 + y="0"
  75 + width="100"
  76 + height="100"
  77 + :fill="getDesign.backgroundColor"
  78 + />
  79 + <g class="height" :transform="`translate(0 ${getWaveHeight})`">
  80 + <use class="wave waveFirst" xlink:href="#wave" :fill="getDesign.waveFirst" x="0" y="0" />
  81 + <use class="wave waveSecond" xlink:href="#wave" :fill="getDesign.waveSecond" x="0" y="2" />
  82 + <use class="wave waveThird" xlink:href="#wave" :fill="getDesign.waveThird" x="0" y="4" />
  83 + </g>
  84 + <rect
  85 + class="waveThird"
  86 + :transform="`translate(0 ${getHeight})`"
  87 + x="0"
  88 + y="0"
  89 + width="100"
  90 + height="100"
  91 + :fill="getDesign.waveThird"
  92 + />
  93 +
  94 + <foreignObject
  95 + x="0"
  96 + y="0"
  97 + width="100"
  98 + height="100"
  99 + text-anchor="middle"
  100 + dominant-baseline="middle"
  101 + >
  102 + <div xmlns="http://www.w3.org/1999/xhtml" class="text">
  103 + <span>{{ currentValue }}</span>
  104 + <span class="ml-1">{{ getDesign.unit }}</span>
  105 + </div>
  106 + </foreignObject>
  107 + </svg>
  108 + </main>
  109 +</template>
  110 +
  111 +<style lang="less" scoped>
  112 + .waves-rect {
  113 + width: 90%;
  114 + height: 90%;
  115 + }
  116 +
  117 + @keyframes move {
  118 + from {
  119 + transform: translate(-90px, 0%);
  120 + }
  121 +
  122 + to {
  123 + transform: translate(85px, 0%);
  124 + }
  125 + }
  126 +
  127 + .wave {
  128 + animation: move 3s linear infinite;
  129 + animation-play-state: running;
  130 + }
  131 +
  132 + .wave:nth-child(1) {
  133 + animation-delay: -2s;
  134 + animation-duration: 9s;
  135 + }
  136 +
  137 + .wave:nth-child(2) {
  138 + animation-delay: -4s;
  139 + animation-duration: 6s;
  140 + }
  141 +
  142 + .wave:nth-child(3) {
  143 + animation-delay: -6s;
  144 + animation-duration: 3s;
  145 + }
  146 +
  147 + .waves-rect > g + rect {
  148 + // transform: translateY(
  149 + // calc(calc(100 - var(--value)) * var(--full-flag) * 1% + var(--full-flag) * 15%)
  150 + // );
  151 + transition: transform linear 1s;
  152 + }
  153 +
  154 + .height {
  155 + // transform: translateY(calc(var(--value) * -1% - 10% + var(--over-min-flag) * 10%));
  156 + transition: transform linear 1s;
  157 + }
  158 +
  159 + .waves-rect .text {
  160 + display: flex;
  161 + justify-content: center;
  162 + align-items: center;
  163 + width: 100%;
  164 + height: 100%;
  165 + color: #fff;
  166 + font-weight: 700;
  167 + }
  168 +</style>
... ...
  1 +import { RectFlowmeteConfig } from './RectFlowmeter';
  2 +
  3 +export const FlowmeterList = [RectFlowmeteConfig];
... ...
  1 +import cloneDeep from 'lodash-es/cloneDeep';
  2 +import { DigitalDashboardComponentConfig } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '/@/views/visual/packages/index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig';
  10 +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  11 +
  12 +export const option: PublicPresetOptions = {
  13 + [ComponentConfigFieldEnum.FONT_COLOR]: '#000',
  14 + [ComponentConfigFieldEnum.UNIT]: 'kw/h',
  15 + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false,
  16 +};
  17 +
  18 +export default class Config extends PublicConfigClass implements CreateComponentType {
  19 + public key: string = DigitalDashboardComponentConfig.key;
  20 +
  21 + public attr = { ...componentInitConfig, w: 340 };
  22 +
  23 + public componentConfig: ConfigType = cloneDeep(DigitalDashboardComponentConfig);
  24 +
  25 + public persetOption = cloneDeep(option);
  26 +
  27 + public option: PublicComponentOptions;
  28 +
  29 + constructor(option: PublicComponentOptions) {
  30 + super();
  31 + this.option = { ...option };
  32 + }
  33 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  3 + import { useForm, BasicForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 + import { option } from './config';
  6 +
  7 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  8 + schemas: [
  9 + {
  10 + field: ComponentConfigFieldEnum.FONT_COLOR,
  11 + label: '数值字体颜色',
  12 + component: 'ColorPicker',
  13 + changeEvent: 'update:value',
  14 + defaultValue: option.fontColor,
  15 + },
  16 + {
  17 + field: ComponentConfigFieldEnum.UNIT,
  18 + label: '数值单位',
  19 + component: 'Input',
  20 + defaultValue: option.unit,
  21 + componentProps: {
  22 + placeholder: '请输入数值单位',
  23 + },
  24 + },
  25 + {
  26 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  27 + label: '显示设备名称',
  28 + component: 'Checkbox',
  29 + defaultValue: option.showDeviceName,
  30 + },
  31 + ],
  32 + showActionButtonGroup: false,
  33 + labelWidth: 120,
  34 + baseColProps: {
  35 + span: 12,
  36 + },
  37 + });
  38 +
  39 + const getFormValues = () => {
  40 + return getFieldsValue();
  41 + };
  42 +
  43 + const setFormValues = (data: Recordable) => {
  44 + return setFieldsValue(data);
  45 + };
  46 +
  47 + defineExpose({
  48 + getFormValues,
  49 + setFormValues,
  50 + resetFormValues: resetFields,
  51 + } as PublicFormInstaceType);
  52 +</script>
  53 +
  54 +<template>
  55 + <BasicForm @register="register" />
  56 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { CreateComponentType } from '/@/views/visual/packages/index.type';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import {
  5 + PublicComponentValueType,
  6 + PublicFormInstaceType,
  7 + } from '/@/views/visual/dataSourceBindPanel/index.type';
  8 + import { commonDataSourceSchemas } from '../../../config/common.config';
  9 +
  10 + defineProps<{
  11 + values: PublicComponentValueType;
  12 + componentConfig: CreateComponentType;
  13 + }>();
  14 +
  15 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  16 + labelWidth: 0,
  17 + showActionButtonGroup: false,
  18 + layout: 'horizontal',
  19 + labelCol: { span: 0 },
  20 + schemas: commonDataSourceSchemas(),
  21 + });
  22 +
  23 + const getFormValues = () => {
  24 + return getFieldsValue();
  25 + };
  26 +
  27 + const setFormValues = (record: Recordable) => {
  28 + return setFieldsValue(record);
  29 + };
  30 +
  31 + defineExpose({
  32 + getFormValues,
  33 + setFormValues,
  34 + validate,
  35 + resetFormValues: resetFields,
  36 + } as PublicFormInstaceType);
  37 +</script>
  38 +
  39 +<template>
  40 + <BasicForm @register="register" />
  41 +</template>
... ...
  1 +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys';
  2 +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type';
  3 +
  4 +const componentKeys = useComponentKeys('DigitalDashboardComponent');
  5 +
  6 +export const DigitalDashboardComponentConfig: ConfigType = {
  7 + ...componentKeys,
  8 + title: '数字仪表盘',
  9 + package: PackagesCategoryEnum.INSTRUMENT,
  10 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type';
  3 + import { option } from './config';
  4 + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket';
  5 + import { ref, computed, unref } from 'vue';
  6 + import { Space } from 'ant-design-vue';
  7 + import { useComponentScale } from '/@/views/visual/packages/hook/useComponentScale';
  8 + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime';
  9 +
  10 + const props = defineProps<{
  11 + config: ComponentPropsConfigType<typeof option>;
  12 + }>();
  13 +
  14 + const currentValue = ref(99.23);
  15 +
  16 + const time = ref<Nullable<number>>(null);
  17 +
  18 + const integerPart = computed(() => {
  19 + let number = unref(currentValue);
  20 + const max = 5;
  21 + if (isNaN(number)) number = 0;
  22 + let _value = number.toString().split('.')[0];
  23 +
  24 + if (_value.length > max) return ''.padStart(max, '9');
  25 + if (_value.length < max) return _value.padStart(max, '0');
  26 +
  27 + return _value;
  28 + });
  29 +
  30 + const decimalPart = computed(() => {
  31 + let number = unref(currentValue);
  32 +
  33 + const keepNumber = 2;
  34 +
  35 + if (isNaN(number)) number = 0;
  36 +
  37 + let _value = number.toString().split('.')[1] || '0';
  38 +
  39 + if (_value.length < keepNumber) return ''.padStart(keepNumber, '0');
  40 +
  41 + if (_value.length > keepNumber) return _value.slice(0, 2);
  42 +
  43 + return _value;
  44 + });
  45 +
  46 + const getDesign = computed(() => {
  47 + const { option, persetOption } = props.config;
  48 + const { componentInfo, attribute, attributeRename } = option;
  49 + const { fontColor: presetFontColor, unit: presetUnit } = persetOption || {};
  50 + const { unit, fontColor } = componentInfo || {};
  51 + return {
  52 + unit: unit ?? presetUnit,
  53 + fontColor: fontColor ?? presetFontColor,
  54 + attribute: attributeRename || attribute,
  55 + };
  56 + });
  57 +
  58 + const updateFn: DataFetchUpdateFn = (message, attribute) => {
  59 + const { data = {} } = message;
  60 + const [latest] = data[attribute] || [];
  61 + const [timespan, value] = latest;
  62 + time.value = timespan;
  63 + currentValue.value = isNaN(value as unknown as number) ? 0 : Number(value);
  64 + };
  65 +
  66 + useDataFetch(props, updateFn);
  67 +
  68 + const { getScale } = useComponentScale(props);
  69 +</script>
  70 +
  71 +<template>
  72 + <main class="w-full h-full flex flex-col justify-center items-center">
  73 + <div class="flex flex-col w-full h-full">
  74 + <div class="flex-1 flex justify-center items-center">
  75 + <div class="flex px-4 items-center transform scale-75" :style="getScale">
  76 + <Space
  77 + justify="end"
  78 + class="justify-end"
  79 + :size="4"
  80 + :style="{
  81 + backgroundColor: '#585357',
  82 + padding: '10px',
  83 + }"
  84 + >
  85 + <div
  86 + v-for="number in integerPart"
  87 + :key="number"
  88 + class="digital-wrapper__int"
  89 + :style="{
  90 + color: getDesign.fontColor,
  91 + }"
  92 + >
  93 + <div class="digital-text__int p-1 text-light-50"> {{ number }}</div>
  94 + </div>
  95 + </Space>
  96 + <div
  97 + class="m-0.5 rounded-1/2"
  98 + style="background-color: #333; width: 6px; height: 6px; align-self: flex-end"
  99 + >
  100 + </div>
  101 + <Space
  102 + justify="end"
  103 + class="justify-end"
  104 + :size="4"
  105 + :style="{
  106 + backgroundColor: '#b74940',
  107 + padding: '10px',
  108 + }"
  109 + >
  110 + <div
  111 + v-for="number in decimalPart"
  112 + :key="number"
  113 + class="digital-wrapper__float"
  114 + :style="{
  115 + color: getDesign.fontColor,
  116 + }"
  117 + >
  118 + <div class="digital-text__float p-1 text-light-50">
  119 + {{ number }}
  120 + </div>
  121 + </div>
  122 + </Space>
  123 + <div class="px-1 font-bold">
  124 + {{ getDesign.unit || 'kw/h' }}
  125 + </div>
  126 + </div>
  127 + </div>
  128 +
  129 + <div class="text-center truncate text-xs text-gray-500">
  130 + <span>{{ getDesign.attribute || '电表' }}</span>
  131 + </div>
  132 +
  133 + <UpdateTime :time="time" />
  134 + </div>
  135 + </main>
  136 +</template>
  137 +
  138 +<style scoped lang="less">
  139 + .digital-wrapper__int {
  140 + border-radius: 1px;
  141 + box-shadow: inset 0 1px 3px 0 rgba(0, 0, 0, 0.7);
  142 + background: url('/@/assets/images/digital-wrapper-bg-int.png') 0 -1px no-repeat;
  143 + padding: 5px;
  144 + background-size: 100% 100%;
  145 + }
  146 +
  147 + .digital-text_int {
  148 + display: inline-block;
  149 + overflow-wrap: break-word;
  150 + color: rgba(255, 255, 255, 1);
  151 + white-space: nowrap;
  152 + text-align: center;
  153 + }
  154 +
  155 + .digital-wrapper__float {
  156 + border-radius: 1px;
  157 + box-shadow: inset 0 1px 3px 0 rgba(112, 22, 15, 1);
  158 + background: url('/@/assets/images/digital-wrapper-bg-float.png') 0 -1px no-repeat;
  159 + padding: 5px;
  160 + background-size: 100% 100%;
  161 + }
  162 +
  163 + .digital-text_float {
  164 + display: inline-block;
  165 + overflow-wrap: break-word;
  166 + color: rgba(255, 255, 255, 1);
  167 + white-space: nowrap;
  168 + text-align: center;
  169 + }
  170 +</style>
... ...
  1 +import cloneDeep from 'lodash-es/cloneDeep';
  2 +import { InstrumentComponent1Config } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '../../../index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '../../../publicConfig';
  10 +import { ComponentConfigFieldEnum } from '../../../enum';
  11 +
  12 +export const option: PublicPresetOptions = {
  13 + [ComponentConfigFieldEnum.FONT_COLOR]: '#FD7347',
  14 + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false,
  15 + [ComponentConfigFieldEnum.UNIT]: '℃',
  16 +};
  17 +
  18 +export default class Config extends PublicConfigClass implements CreateComponentType {
  19 + public key: string = InstrumentComponent1Config.key;
  20 +
  21 + public attr = { ...componentInitConfig };
  22 +
  23 + public componentConfig: ConfigType = cloneDeep(InstrumentComponent1Config);
  24 +
  25 + public persetOption = cloneDeep(option);
  26 +
  27 + public option: PublicComponentOptions;
  28 +
  29 + constructor(option: PublicComponentOptions) {
  30 + super();
  31 + this.option = { ...option };
  32 + }
  33 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentConfigFieldEnum } from '../../../enum';
  3 + import { option } from './config';
  4 + import { useForm, BasicForm } from '/@/components/Form';
  5 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  6 +
  7 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  8 + schemas: [
  9 + {
  10 + field: ComponentConfigFieldEnum.FONT_COLOR,
  11 + label: '数值字体颜色',
  12 + component: 'ColorPicker',
  13 + changeEvent: 'update:value',
  14 + defaultValue: option.fontColor,
  15 + },
  16 + {
  17 + field: ComponentConfigFieldEnum.UNIT,
  18 + label: '数值单位',
  19 + component: 'Input',
  20 + defaultValue: option.unit,
  21 + },
  22 + {
  23 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  24 + label: '显示设备名称',
  25 + component: 'Checkbox',
  26 + defaultValue: option.showDeviceName,
  27 + },
  28 + ],
  29 + showActionButtonGroup: false,
  30 + labelWidth: 120,
  31 + baseColProps: {
  32 + span: 12,
  33 + },
  34 + });
  35 +
  36 + const getFormValues = () => {
  37 + return getFieldsValue();
  38 + };
  39 +
  40 + const setFormValues = (data: Recordable) => {
  41 + return setFieldsValue(data);
  42 + };
  43 +
  44 + defineExpose({
  45 + getFormValues,
  46 + setFormValues,
  47 + resetFormValues: resetFields,
  48 + } as PublicFormInstaceType);
  49 +</script>
  50 +
  51 +<template>
  52 + <BasicForm @register="register" />
  53 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { commonDataSourceSchemas } from '../../../config/common.config';
  3 + import { CreateComponentType } from '../../../index.type';
  4 + import { BasicForm, useForm } from '/@/components/Form';
  5 + import {
  6 + PublicComponentValueType,
  7 + PublicFormInstaceType,
  8 + } from '/@/views/visual/dataSourceBindPanel/index.type';
  9 +
  10 + defineProps<{
  11 + values: PublicComponentValueType;
  12 + componentConfig: CreateComponentType;
  13 + }>();
  14 +
  15 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  16 + labelWidth: 0,
  17 + showActionButtonGroup: false,
  18 + layout: 'horizontal',
  19 + labelCol: { span: 0 },
  20 + schemas: commonDataSourceSchemas(),
  21 + });
  22 +
  23 + const getFormValues = () => {
  24 + return getFieldsValue();
  25 + };
  26 +
  27 + const setFormValues = (record: Recordable) => {
  28 + return setFieldsValue(record);
  29 + };
  30 +
  31 + defineExpose({
  32 + getFormValues,
  33 + setFormValues,
  34 + validate,
  35 + resetFormValues: resetFields,
  36 + } as PublicFormInstaceType);
  37 +</script>
  38 +
  39 +<template>
  40 + <BasicForm @register="register" />
  41 +</template>
... ...
  1 +import { useComponentKeys } from '../../../hook/useComponentKeys';
  2 +import { ConfigType, PackagesCategoryEnum } from '../../../index.type';
  3 +
  4 +const componentKeys = useComponentKeys('InstrumentComponent1');
  5 +
  6 +export const InstrumentComponent1Config: ConfigType = {
  7 + ...componentKeys,
  8 + title: '仪表盘',
  9 + package: PackagesCategoryEnum.INSTRUMENT,
  10 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { EChartsOption, ECharts, init } from 'echarts';
  3 + import { onMounted } from 'vue';
  4 + import { unref } from 'vue';
  5 + import { ref } from 'vue';
  6 + import { useDataFetch } from '../../../hook/useSocket';
  7 + import { ComponentPropsConfigType, DataFetchUpdateFn } from '../../../index.type';
  8 + import { option } from './config';
  9 + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime';
  10 + import { useIntervalFn } from '@vueuse/core';
  11 + import { computed } from 'vue';
  12 + import { useComponentScale } from '../../../hook/useComponentScale';
  13 + import { nextTick } from 'vue';
  14 + import { DeviceName } from '/@/views/visual/commonComponents/DeviceName';
  15 +
  16 + const props = defineProps<{
  17 + config: ComponentPropsConfigType<typeof option>;
  18 + }>();
  19 +
  20 + const chartRefEl = ref<Nullable<HTMLDivElement>>(null);
  21 +
  22 + const chartInstance = ref<Nullable<ECharts>>(null);
  23 +
  24 + const time = ref<Nullable<number>>(null);
  25 +
  26 + const getDesign = computed(() => {
  27 + const { option, persetOption } = props.config;
  28 + const { componentInfo, attribute, attributeRename } = option;
  29 + const { fontColor: presetFontColor, unit: presetUnit } = persetOption || {};
  30 + const { unit, fontColor } = componentInfo || {};
  31 + return {
  32 + unit: unit ?? presetUnit,
  33 + fontColor: fontColor ?? presetFontColor,
  34 + attribute: attributeRename || attribute,
  35 + };
  36 + });
  37 +
  38 + const options = (): EChartsOption => {
  39 + const { unit, fontColor } = unref(getDesign);
  40 + return {
  41 + series: [
  42 + {
  43 + type: 'gauge',
  44 + radius: '50%',
  45 + center: ['50%', '60%'],
  46 + startAngle: 200,
  47 + endAngle: -20,
  48 + min: 0,
  49 + max: 100,
  50 + splitNumber: 10,
  51 + itemStyle: {
  52 + color: fontColor,
  53 + },
  54 + progress: {
  55 + show: true,
  56 + width: 30,
  57 + },
  58 + pointer: {
  59 + show: false,
  60 + },
  61 + axisLine: {
  62 + lineStyle: {
  63 + width: 30,
  64 + },
  65 + },
  66 + axisTick: {
  67 + distance: -35,
  68 + splitNumber: 5,
  69 + lineStyle: {
  70 + width: 2,
  71 + color: '#999',
  72 + },
  73 + },
  74 + splitLine: {
  75 + distance: -40,
  76 + length: 10,
  77 + lineStyle: {
  78 + width: 3,
  79 + color: '#999',
  80 + },
  81 + },
  82 + axisLabel: {
  83 + distance: 0,
  84 + color: '#999',
  85 + },
  86 + anchor: {
  87 + show: false,
  88 + },
  89 + title: {
  90 + show: false,
  91 + },
  92 + detail: {
  93 + valueAnimation: true,
  94 + width: '60%',
  95 + lineHeight: 10,
  96 + borderRadius: 8,
  97 + offsetCenter: [0, '30%'],
  98 + fontSize: 14,
  99 + fontWeight: 'bolder',
  100 + formatter: `{value} ${unit ?? ''}`,
  101 + color: fontColor || 'inherit',
  102 + },
  103 + data: [
  104 + {
  105 + value: 20,
  106 + },
  107 + ],
  108 + },
  109 + {
  110 + type: 'gauge',
  111 + radius: '50%',
  112 + center: ['50%', '60%'],
  113 + startAngle: 200,
  114 + endAngle: -20,
  115 + min: 0,
  116 + max: 100,
  117 + itemStyle: {
  118 + color: fontColor,
  119 + },
  120 + progress: {
  121 + show: true,
  122 + width: 8,
  123 + },
  124 + pointer: {
  125 + show: false,
  126 + },
  127 + axisLine: {
  128 + show: false,
  129 + },
  130 + axisTick: {
  131 + show: false,
  132 + },
  133 + splitLine: {
  134 + show: false,
  135 + },
  136 + axisLabel: {
  137 + show: false,
  138 + },
  139 + detail: {
  140 + show: false,
  141 + },
  142 + data: [
  143 + {
  144 + value: 20,
  145 + },
  146 + ],
  147 + },
  148 + ],
  149 + };
  150 + };
  151 +
  152 + const updateChart = (value: number) => {
  153 + unref(chartInstance)?.setOption({
  154 + series: [{ data: [{ value: value.toFixed(2) }] }, { data: [{ value: value.toFixed(2) }] }],
  155 + } as EChartsOption);
  156 + };
  157 +
  158 + const initial = () => {
  159 + chartInstance.value = init(unref(chartRefEl)!);
  160 + chartInstance.value.setOption(options());
  161 + };
  162 +
  163 + const randomFn = () => {
  164 + useIntervalFn(() => {
  165 + const value = (Math.random() * 100).toFixed(0);
  166 + unref(chartInstance)?.setOption({
  167 + series: [{ data: [{ value }] }, { data: [{ value }] }],
  168 + } as EChartsOption);
  169 + }, 3000);
  170 + };
  171 +
  172 + const updateFn: DataFetchUpdateFn = (message, attribute) => {
  173 + const { data = {} } = message;
  174 + const [latest] = data[attribute] || [];
  175 + const [timespan, value] = latest;
  176 + time.value = timespan;
  177 + updateChart(isNaN(value as unknown as number) ? 0 : Number(value));
  178 + };
  179 +
  180 + useDataFetch(props, updateFn);
  181 +
  182 + onMounted(() => {
  183 + initial();
  184 + !props.config.option.uuid && randomFn();
  185 + });
  186 +
  187 + const resize = async () => {
  188 + await nextTick();
  189 + unref(chartInstance)?.resize();
  190 + };
  191 +
  192 + useComponentScale(props, resize);
  193 +</script>
  194 +
  195 +<template>
  196 + <main class="w-full h-full flex flex-col justify-center items-center">
  197 + <DeviceName :config="config" />
  198 + <div ref="chartRefEl" class="flex-1 w-full h-full"> </div>
  199 + <div class="text-gray-500 text-xs text-center truncate">{{
  200 + getDesign.attribute || '温度'
  201 + }}</div>
  202 + <UpdateTime :time="time" />
  203 + </main>
  204 +</template>
... ...
  1 +import cloneDeep from 'lodash-es/cloneDeep';
  2 +import { InstrumentComponent2Config } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '/@/views/visual/packages/index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig';
  10 +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  11 +
  12 +export enum Gradient {
  13 + FIRST = 'first',
  14 + SECOND = 'second',
  15 + THIRD = 'third',
  16 +}
  17 +export enum GradientColor {
  18 + FIRST = '#67e0e3',
  19 + SECOND = '#37a2da',
  20 + THIRD = '#fd666d',
  21 +}
  22 +
  23 +export const option: PublicPresetOptions = {
  24 + [ComponentConfigFieldEnum.FONT_COLOR]: GradientColor.THIRD,
  25 + [ComponentConfigFieldEnum.GRADIENT_INFO]: [
  26 + { key: Gradient.FIRST, value: 30, color: GradientColor.FIRST },
  27 + { key: Gradient.SECOND, value: 70, color: GradientColor.SECOND },
  28 + { key: Gradient.THIRD, value: 100, color: GradientColor.THIRD },
  29 + ],
  30 + [ComponentConfigFieldEnum.UNIT]: 'km/h',
  31 + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false,
  32 +};
  33 +
  34 +export default class Config extends PublicConfigClass implements CreateComponentType {
  35 + public key: string = InstrumentComponent2Config.key;
  36 +
  37 + public attr = { ...componentInitConfig };
  38 +
  39 + public componentConfig: ConfigType = cloneDeep(InstrumentComponent2Config);
  40 +
  41 + public persetOption = cloneDeep(option);
  42 +
  43 + public option: PublicComponentOptions;
  44 +
  45 + constructor(option: PublicComponentOptions) {
  46 + super();
  47 + this.option = { ...option };
  48 + }
  49 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  3 + import { useForm, BasicForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 + import { Gradient, GradientColor, option } from './config';
  6 + import { ComponentInfo } from '/@/views/visual/palette/types';
  7 +
  8 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  9 + schemas: [
  10 + {
  11 + field: ComponentConfigFieldEnum.FONT_COLOR,
  12 + label: '数值字体颜色',
  13 + component: 'ColorPicker',
  14 + changeEvent: 'update:value',
  15 + defaultValue: option.fontColor,
  16 + },
  17 + {
  18 + field: ComponentConfigFieldEnum.UNIT,
  19 + label: '数值单位',
  20 + component: 'Input',
  21 + defaultValue: option.unit,
  22 + componentProps: {
  23 + placeholder: '请输入数值单位',
  24 + },
  25 + },
  26 + {
  27 + field: ComponentConfigFieldEnum.FIRST_PHASE_COLOR,
  28 + label: '一阶段颜色',
  29 + component: 'ColorPicker',
  30 + changeEvent: 'update:value',
  31 + defaultValue: GradientColor.FIRST,
  32 + },
  33 + {
  34 + field: ComponentConfigFieldEnum.FIRST_PHASE_VALUE,
  35 + label: '一阶段阀值',
  36 + component: 'InputNumber',
  37 + componentProps: {
  38 + placeholder: '请输入一阶段阀值',
  39 + min: 0,
  40 + },
  41 + },
  42 + {
  43 + field: ComponentConfigFieldEnum.SECOND_PHASE_COLOR,
  44 + label: '二阶段颜色',
  45 + component: 'ColorPicker',
  46 + changeEvent: 'update:value',
  47 + defaultValue: GradientColor.SECOND,
  48 + },
  49 + {
  50 + field: ComponentConfigFieldEnum.SECOND_PHASE_VALUE,
  51 + label: '二阶段阀值',
  52 + component: 'InputNumber',
  53 + componentProps: ({ formModel }) => {
  54 + return {
  55 + placeholder: '请输入二阶段阀值',
  56 + min: formModel[ComponentConfigFieldEnum.FIRST_PHASE_VALUE],
  57 + };
  58 + },
  59 + },
  60 + {
  61 + field: ComponentConfigFieldEnum.THIRD_PHASE_COLOR,
  62 + label: '三阶段颜色',
  63 + component: 'ColorPicker',
  64 + changeEvent: 'update:value',
  65 + defaultValue: GradientColor.THIRD,
  66 + },
  67 + {
  68 + field: ComponentConfigFieldEnum.THIRD_PHASE_VALUE,
  69 + label: '三阶段阀值',
  70 + component: 'InputNumber',
  71 + componentProps: ({ formModel }) => {
  72 + return {
  73 + placeholder: '请输入三阶段阀值',
  74 + min: formModel[ComponentConfigFieldEnum.SECOND_PHASE_VALUE],
  75 + };
  76 + },
  77 + },
  78 + {
  79 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  80 + label: '显示设备名称',
  81 + component: 'Checkbox',
  82 + defaultValue: option.showDeviceName,
  83 + },
  84 + ],
  85 + showActionButtonGroup: false,
  86 + labelWidth: 120,
  87 + baseColProps: {
  88 + span: 12,
  89 + },
  90 + });
  91 +
  92 + const getFormValues = () => {
  93 + const value = getFieldsValue();
  94 + return {
  95 + gradientInfo: [
  96 + {
  97 + key: Gradient.FIRST,
  98 + value: value[ComponentConfigFieldEnum.FIRST_PHASE_VALUE],
  99 + color: value[ComponentConfigFieldEnum.FIRST_PHASE_COLOR],
  100 + },
  101 + {
  102 + key: Gradient.SECOND,
  103 + value: value[ComponentConfigFieldEnum.SECOND_PHASE_VALUE],
  104 + color: value[ComponentConfigFieldEnum.SECOND_PHASE_COLOR],
  105 + },
  106 + {
  107 + key: Gradient.THIRD,
  108 + value: value[ComponentConfigFieldEnum.THIRD_PHASE_VALUE],
  109 + color: value[ComponentConfigFieldEnum.THIRD_PHASE_COLOR],
  110 + },
  111 + ],
  112 + fontColor: value[ComponentConfigFieldEnum.FONT_COLOR],
  113 + unit: value[ComponentConfigFieldEnum.UNIT],
  114 + showDeviceName: value[ComponentConfigFieldEnum.SHOW_DEVICE_NAME],
  115 + } as ComponentInfo;
  116 + };
  117 +
  118 + const setFormValues = (data: ComponentInfo) => {
  119 + const { gradientInfo, unit, fontColor, showDeviceName } = data;
  120 + const firstRecord = gradientInfo.find((item) => item.key === Gradient.FIRST);
  121 + const secondRecord = gradientInfo.find((item) => item.key === Gradient.SECOND);
  122 + const thirdRecord = gradientInfo.find((item) => item.key === Gradient.THIRD);
  123 + const value = {
  124 + [ComponentConfigFieldEnum.UNIT]: unit,
  125 + [ComponentConfigFieldEnum.FONT_COLOR]: fontColor,
  126 + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: showDeviceName,
  127 + [ComponentConfigFieldEnum.FIRST_PHASE_VALUE]: firstRecord?.value,
  128 + [ComponentConfigFieldEnum.FIRST_PHASE_COLOR]: firstRecord?.color,
  129 + [ComponentConfigFieldEnum.SECOND_PHASE_VALUE]: secondRecord?.value,
  130 + [ComponentConfigFieldEnum.SECOND_PHASE_COLOR]: secondRecord?.color,
  131 + [ComponentConfigFieldEnum.THIRD_PHASE_VALUE]: thirdRecord?.value,
  132 + [ComponentConfigFieldEnum.THIRD_PHASE_COLOR]: thirdRecord?.color,
  133 + };
  134 + return setFieldsValue(value);
  135 + };
  136 +
  137 + defineExpose({
  138 + getFormValues,
  139 + setFormValues,
  140 + resetFormValues: resetFields,
  141 + } as PublicFormInstaceType);
  142 +</script>
  143 +
  144 +<template>
  145 + <BasicForm @register="register" />
  146 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { CreateComponentType } from '/@/views/visual/packages/index.type';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import {
  5 + PublicComponentValueType,
  6 + PublicFormInstaceType,
  7 + } from '/@/views/visual/dataSourceBindPanel/index.type';
  8 + import { commonDataSourceSchemas } from '../../../config/common.config';
  9 +
  10 + defineProps<{
  11 + values: PublicComponentValueType;
  12 + componentConfig: CreateComponentType;
  13 + }>();
  14 +
  15 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  16 + labelWidth: 0,
  17 + showActionButtonGroup: false,
  18 + layout: 'horizontal',
  19 + labelCol: { span: 0 },
  20 + schemas: commonDataSourceSchemas(),
  21 + });
  22 +
  23 + const getFormValues = () => {
  24 + return getFieldsValue();
  25 + };
  26 +
  27 + const setFormValues = (record: Recordable) => {
  28 + return setFieldsValue(record);
  29 + };
  30 +
  31 + defineExpose({
  32 + getFormValues,
  33 + setFormValues,
  34 + validate,
  35 + resetFormValues: resetFields,
  36 + } as PublicFormInstaceType);
  37 +</script>
  38 +
  39 +<template>
  40 + <BasicForm @register="register" />
  41 +</template>
... ...
  1 +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys';
  2 +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type';
  3 +
  4 +const componentKeys = useComponentKeys('InstrumentComponent2');
  5 +
  6 +export const InstrumentComponent2Config: ConfigType = {
  7 + ...componentKeys,
  8 + title: '阶段仪表盘',
  9 + package: PackagesCategoryEnum.INSTRUMENT,
  10 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type';
  3 + import { Gradient, GradientColor, option } from './config';
  4 + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket';
  5 + import { ECharts, EChartsOption, init } from 'echarts';
  6 + import { ref, unref, onMounted, computed } from 'vue';
  7 + import { isArray } from '/@/utils/is';
  8 + import { ComponentInfoGradientInfoType } from '/@/views/visual/palette/types';
  9 + import { useComponentScale } from '/@/views/visual/packages/hook/useComponentScale';
  10 + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime';
  11 + import { useIntervalFn } from '@vueuse/core';
  12 + import { nextTick } from 'vue';
  13 +
  14 + const props = defineProps<{
  15 + config: ComponentPropsConfigType<typeof option>;
  16 + }>();
  17 +
  18 + const time = ref<Nullable<number>>(null);
  19 +
  20 + useComponentScale(props, async () => {
  21 + await nextTick();
  22 + unref(chartInstance)?.resize();
  23 + });
  24 +
  25 + const chartRefEl = ref<Nullable<HTMLDivElement>>(null);
  26 +
  27 + const chartInstance = ref<Nullable<ECharts>>(null);
  28 +
  29 + const getDesign = computed(() => {
  30 + const { option, persetOption } = props.config;
  31 + const { componentInfo, attributeRename, attribute } = option;
  32 +
  33 + const {
  34 + fontColor: presetFontColor,
  35 + unit: presetUnit,
  36 + gradientInfo: presetGradientInfo,
  37 + } = persetOption || {};
  38 +
  39 + const { unit, fontColor, gradientInfo } = componentInfo || {};
  40 + return {
  41 + unit: unit ?? presetUnit,
  42 + fontColor: fontColor ?? presetFontColor,
  43 + gradientInfo: gradientInfo ?? presetGradientInfo,
  44 + attribute: attributeRename || attribute,
  45 + };
  46 + });
  47 +
  48 + const getGradient = (key: Gradient, record: ComponentInfoGradientInfoType[] = []) => {
  49 + if (!isArray(record)) return;
  50 + return record.find((item) => item.key === key);
  51 + };
  52 +
  53 + const options = (): EChartsOption => {
  54 + const { gradientInfo, unit, fontColor } = unref(getDesign);
  55 + const firstRecord = getGradient(Gradient.FIRST, gradientInfo);
  56 + const secondRecord = getGradient(Gradient.SECOND, gradientInfo);
  57 + const thirdRecord = getGradient(Gradient.THIRD, gradientInfo);
  58 +
  59 + let max = thirdRecord?.value || secondRecord?.value || firstRecord?.value || 100;
  60 +
  61 + max = Number(
  62 + 1 +
  63 + Array(String(max).length - 1)
  64 + .fill(0)
  65 + .join('')
  66 + );
  67 +
  68 + const firstGradient = firstRecord?.value ? firstRecord.value / max : 0.3;
  69 + const secondGradient = secondRecord?.value ? secondRecord.value / max : 0.7;
  70 +
  71 + return {
  72 + series: [
  73 + {
  74 + type: 'gauge',
  75 + min: 0,
  76 + max,
  77 + axisLine: {
  78 + lineStyle: {
  79 + width: 20,
  80 + color: [
  81 + [firstGradient, firstRecord?.color || GradientColor.FIRST],
  82 + [secondGradient, secondRecord?.color || GradientColor.SECOND],
  83 + [1, thirdRecord?.color || GradientColor.THIRD],
  84 + ],
  85 + },
  86 + },
  87 + pointer: {
  88 + itemStyle: {
  89 + color: 'inherit',
  90 + },
  91 + },
  92 + axisTick: {
  93 + distance: -30,
  94 + length: 8,
  95 + splitNumber: max / 100,
  96 + lineStyle: {
  97 + color: '#fff',
  98 + width: 2,
  99 + },
  100 + },
  101 + splitLine: {
  102 + distance: -10,
  103 + length: 30,
  104 + lineStyle: {
  105 + color: '#fff',
  106 + width: 4,
  107 + },
  108 + },
  109 + axisLabel: {
  110 + color: 'inherit',
  111 + distance: 5,
  112 + fontSize: 6,
  113 + },
  114 + detail: {
  115 + valueAnimation: true,
  116 + formatter: `{value} ${unit ?? ''}`,
  117 + color: fontColor || 'inherit',
  118 + offsetCenter: [0, '70%'],
  119 + fontSize: 14,
  120 + },
  121 + data: [
  122 + {
  123 + value: 20,
  124 + },
  125 + ],
  126 + },
  127 + ],
  128 + };
  129 + };
  130 +
  131 + const updateChartFn = (value: number) => {
  132 + unref(chartInstance)?.setOption({
  133 + series: [{ data: [{ value: value.toFixed(2) }] }],
  134 + } as EChartsOption);
  135 + };
  136 +
  137 + const initial = () => {
  138 + chartInstance.value = init(unref(chartRefEl)!);
  139 + chartInstance.value.setOption(options());
  140 + };
  141 +
  142 + const randomFn = () => {
  143 + useIntervalFn(() => {
  144 + const value = (Math.random() * 100).toFixed(0);
  145 + unref(chartInstance)?.setOption({
  146 + series: [{ data: [{ value }] }],
  147 + } as EChartsOption);
  148 + }, 3000);
  149 + };
  150 +
  151 + const updateFn: DataFetchUpdateFn = (message, attribute) => {
  152 + const { data = {} } = message;
  153 + const [latest] = data[attribute] || [];
  154 + const [timespan, value] = latest;
  155 + time.value = timespan;
  156 + updateChartFn(isNaN(value as unknown as number) ? 0 : Number(value));
  157 + };
  158 +
  159 + useDataFetch(props, updateFn);
  160 +
  161 + onMounted(() => {
  162 + initial();
  163 + !props.config.option.uuid && randomFn();
  164 + });
  165 +</script>
  166 +
  167 +<template>
  168 + <main class="w-full h-full flex flex-col justify-center items-center">
  169 + <div ref="chartRefEl" class="flex-1 w-full h-full"> </div>
  170 + <div class="text-center text-gray-500 text-xs truncate">
  171 + {{ getDesign.attribute || '速度' }}
  172 + </div>
  173 + <UpdateTime :time="time" />
  174 + </main>
  175 +</template>
... ...
  1 +import { DigitalDashboardComponentConfig } from './DigitalDashboardComponent';
  2 +import { InstrumentComponent1Config } from './InstrumentComponent1';
  3 +import { InstrumentComponent2Config } from './InstrumentComponent2';
  4 +
  5 +export const InstrumentList = [
  6 + InstrumentComponent1Config,
  7 + InstrumentComponent2Config,
  8 + DigitalDashboardComponentConfig,
  9 +];
... ...
  1 +<script lang="ts" setup>
  2 + import { BasicForm, useForm } from '/@/components/Form';
  3 + import { BasicModal, useModalInner } from '/@/components/Modal';
  4 + import { formSchema, getHistorySearchParams, SchemaFiled } from './history.config';
  5 + import { HistoryModalOkEmitParams } from './type';
  6 + import { ref } from 'vue';
  7 + import { getAllDeviceByOrg } from '/@/api/dataBoard';
  8 + import { getDeviceHistoryInfo } from '/@/api/alarm/position';
  9 + import { DataSource } from '/@/views/visual/palette/types';
  10 + import { cloneDeep } from 'lodash-es';
  11 +
  12 + const emit = defineEmits(['register', 'ok']);
  13 +
  14 + const [registerForm, { updateSchema, setFieldsValue, validate, getFieldsValue }] = useForm({
  15 + schemas: formSchema(),
  16 + showActionButtonGroup: false,
  17 + fieldMapToTime: [
  18 + [SchemaFiled.DATE_RANGE, [SchemaFiled.START_TS, SchemaFiled.END_TS], 'YYYY-MM-DD HH:mm:ss'],
  19 + ],
  20 + });
  21 +
  22 + const [registerModal, { closeModal }] = useModalInner(async (dataSource: DataSource[]) => {
  23 + try {
  24 + dataSource = cloneDeep(dataSource);
  25 + if (dataSource.length < 2) return;
  26 + dataSource = dataSource.splice(0, 2);
  27 + const deviceRecord = dataSource?.at(0) || ({} as DataSource);
  28 + if (!deviceRecord.organizationId) return;
  29 + const deviceList = await getAllDeviceByOrg(
  30 + deviceRecord.organizationId,
  31 + deviceRecord.deviceProfileId
  32 + );
  33 + const options = deviceList
  34 + .filter((item) => item.tbDeviceId === deviceRecord.deviceId)
  35 + .map((item) => ({ ...item, label: item.name, value: item.tbDeviceId }));
  36 +
  37 + const attKey = dataSource.map((item) => ({
  38 + ...item,
  39 + label: item.attribute,
  40 + value: item.attribute,
  41 + }));
  42 + updateSchema([
  43 + {
  44 + field: SchemaFiled.DEVICE_ID,
  45 + componentProps: {
  46 + options,
  47 + },
  48 + },
  49 + {
  50 + field: SchemaFiled.KEYS,
  51 + component: 'Select',
  52 + defaultValue: attKey.map((item) => item.value),
  53 + componentProps: {
  54 + options: attKey,
  55 + mode: 'multiple',
  56 + disabled: true,
  57 + },
  58 + },
  59 + ]);
  60 +
  61 + setFieldsValue({
  62 + [SchemaFiled.DEVICE_ID]: deviceRecord.deviceId,
  63 + [SchemaFiled.KEYS]: attKey.map((item) => item.value),
  64 + });
  65 + } catch (error) {
  66 + throw error;
  67 + }
  68 + });
  69 +
  70 + const validEffective = (value = '') => {
  71 + return !!(value && !isNaN(value as unknown as number));
  72 + };
  73 + const loading = ref(false);
  74 + const handleOk = async () => {
  75 + try {
  76 + await validate();
  77 + let value = getFieldsValue();
  78 +
  79 + value = getHistorySearchParams(value);
  80 +
  81 + loading.value = true;
  82 +
  83 + const res = await getDeviceHistoryInfo({
  84 + ...value,
  85 + [SchemaFiled.KEYS]: value[SchemaFiled.KEYS].join(','),
  86 + });
  87 +
  88 + let timespanList = Object.keys(res).reduce((prev, next) => {
  89 + const ts = res[next].map((item) => item.ts);
  90 + return [...prev, ...ts];
  91 + }, [] as number[]);
  92 + timespanList = [...new Set(timespanList)];
  93 +
  94 + const track: Record<'lng' | 'lat', number>[] = [];
  95 + const keys = Object.keys(res);
  96 +
  97 + for (const ts of timespanList) {
  98 + const list: { ts: number; value: number }[] = [];
  99 + for (const key of keys) {
  100 + const record = res[key].find((item) => ts === item.ts);
  101 + if (!validEffective(record?.value)) {
  102 + continue;
  103 + }
  104 + list.push(record as any);
  105 + }
  106 +
  107 + if (list.length === 2 && list.every(Boolean)) {
  108 + const lng = list.at(0)?.value;
  109 + const lat = list.at(1)?.value;
  110 + if (lng && lat) track.push({ lng, lat });
  111 + }
  112 + }
  113 +
  114 + emit('ok', { track, value } as HistoryModalOkEmitParams);
  115 + closeModal();
  116 + } catch (error) {
  117 + throw error;
  118 + } finally {
  119 + loading.value = false;
  120 + }
  121 + };
  122 +</script>
  123 +
  124 +<template>
  125 + <BasicModal
  126 + title="历史轨迹"
  127 + @register="registerModal"
  128 + @ok="handleOk"
  129 + :ok-button-props="{ loading }"
  130 + >
  131 + <BasicForm @register="registerForm" />
  132 + </BasicModal>
  133 +</template>
... ...
  1 +import cloneDeep from 'lodash-es/cloneDeep';
  2 +import { MapComponentTrackHistoryConfig } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '/@/views/visual/packages/index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig';
  10 +
  11 +export const option: PublicPresetOptions = {
  12 + componetDesign: false,
  13 + multipleDataSourceComponent: true,
  14 +};
  15 +
  16 +export default class Config extends PublicConfigClass implements CreateComponentType {
  17 + public key: string = MapComponentTrackHistoryConfig.key;
  18 +
  19 + public attr = { ...componentInitConfig };
  20 +
  21 + public componentConfig: ConfigType = cloneDeep(MapComponentTrackHistoryConfig);
  22 +
  23 + public persetOption = cloneDeep(option);
  24 +
  25 + public option: PublicComponentOptions;
  26 +
  27 + constructor(option: PublicComponentOptions) {
  28 + super();
  29 + this.option = { ...option };
  30 + }
  31 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  3 + import { useForm, BasicForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 +
  6 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  7 + schemas: [
  8 + {
  9 + field: ComponentConfigFieldEnum.FONT_COLOR,
  10 + label: '数值字体颜色',
  11 + component: 'ColorPicker',
  12 + changeEvent: 'update:value',
  13 + componentProps: {
  14 + defaultValue: '#FD7347',
  15 + },
  16 + },
  17 + {
  18 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  19 + label: '显示设备名称',
  20 + component: 'Checkbox',
  21 + },
  22 + ],
  23 + showActionButtonGroup: false,
  24 + labelWidth: 120,
  25 + baseColProps: {
  26 + span: 12,
  27 + },
  28 + });
  29 +
  30 + const getFormValues = () => {
  31 + return getFieldsValue();
  32 + };
  33 +
  34 + const setFormValues = (data: Recordable) => {
  35 + return setFieldsValue(data);
  36 + };
  37 +
  38 + defineExpose({
  39 + getFormValues,
  40 + setFormValues,
  41 + resetFormValues: resetFields,
  42 + } as PublicFormInstaceType);
  43 +</script>
  44 +
  45 +<template>
  46 + <BasicForm @register="register" />
  47 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { CreateComponentType } from '/@/views/visual/packages/index.type';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import {
  5 + PublicComponentValueType,
  6 + PublicFormInstaceType,
  7 + } from '/@/views/visual/dataSourceBindPanel/index.type';
  8 + import { commonDataSourceSchemas } from '../../../config/common.config';
  9 +
  10 + defineProps<{
  11 + values: PublicComponentValueType;
  12 + componentConfig: CreateComponentType;
  13 + }>();
  14 +
  15 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  16 + labelWidth: 0,
  17 + showActionButtonGroup: false,
  18 + layout: 'horizontal',
  19 + labelCol: { span: 0 },
  20 + schemas: commonDataSourceSchemas(),
  21 + });
  22 +
  23 + const getFormValues = () => {
  24 + return getFieldsValue();
  25 + };
  26 +
  27 + const setFormValues = (record: Recordable) => {
  28 + return setFieldsValue(record);
  29 + };
  30 +
  31 + defineExpose({
  32 + getFormValues,
  33 + setFormValues,
  34 + validate,
  35 + resetFormValues: resetFields,
  36 + } as PublicFormInstaceType);
  37 +</script>
  38 +
  39 +<template>
  40 + <BasicForm @register="register" />
  41 +</template>
... ...
  1 +import moment from 'moment';
  2 +import { Moment } from 'moment';
  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 + return [
  40 + {
  41 + field: SchemaFiled.DEVICE_ID,
  42 + label: '设备名称',
  43 + component: 'Select',
  44 + rules: [{ required: true, message: '设备名称为必选项', type: 'string' }],
  45 + componentProps({ formActionType }) {
  46 + const { setFieldsValue } = formActionType;
  47 + return {
  48 + placeholder: '请选择设备',
  49 + onChange() {
  50 + setFieldsValue({ [SchemaFiled.KEYS]: null });
  51 + },
  52 + };
  53 + },
  54 + },
  55 + {
  56 + field: SchemaFiled.WAY,
  57 + label: '查询方式',
  58 + component: 'RadioGroup',
  59 + defaultValue: QueryWay.LATEST,
  60 + componentProps({ formActionType }) {
  61 + const { setFieldsValue } = formActionType;
  62 + return {
  63 + options: [
  64 + { label: '最后', value: QueryWay.LATEST },
  65 + { label: '时间段', value: QueryWay.TIME_PERIOD },
  66 + ],
  67 + onChange(event: ChangeEvent) {
  68 + (event.target as HTMLInputElement).value === QueryWay.LATEST
  69 + ? setFieldsValue({
  70 + [SchemaFiled.DATE_RANGE]: [],
  71 + [SchemaFiled.START_TS]: null,
  72 + [SchemaFiled.END_TS]: null,
  73 + })
  74 + : setFieldsValue({ [SchemaFiled.START_TS]: null });
  75 + },
  76 + getPopupContainer: () => document.body,
  77 + };
  78 + },
  79 + },
  80 + {
  81 + field: SchemaFiled.START_TS,
  82 + label: '最后数据',
  83 + component: 'Select',
  84 + ifShow({ values }) {
  85 + return values[SchemaFiled.WAY] === QueryWay.LATEST;
  86 + },
  87 + componentProps({ formActionType }) {
  88 + const { setFieldsValue } = formActionType;
  89 + return {
  90 + options: intervalOption,
  91 + onChange() {
  92 + setFieldsValue({ [SchemaFiled.INTERVAL]: null });
  93 + },
  94 + getPopupContainer: () => document.body,
  95 + };
  96 + },
  97 + rules: [{ required: true, message: '最后数据为必选项', type: 'number' }],
  98 + },
  99 + {
  100 + field: SchemaFiled.DATE_RANGE,
  101 + label: '时间段',
  102 + component: 'RangePicker',
  103 + ifShow({ values }) {
  104 + return values[SchemaFiled.WAY] === QueryWay.TIME_PERIOD;
  105 + },
  106 + rules: [{ required: true, message: '时间段为必选项' }],
  107 + componentProps({ formActionType }) {
  108 + const { setFieldsValue } = formActionType;
  109 + let dates: Moment[] = [];
  110 + return {
  111 + showTime: {
  112 + defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
  113 + },
  114 + onCalendarChange(value: Moment[]) {
  115 + dates = value;
  116 + },
  117 + disabledDate(current: Moment) {
  118 + if (!dates || dates.length === 0 || !current) {
  119 + return false;
  120 + }
  121 + const diffDate = current.diff(dates[0], 'years', true);
  122 + return Math.abs(diffDate) > 1;
  123 + },
  124 + onChange() {
  125 + dates = [];
  126 + setFieldsValue({ [SchemaFiled.INTERVAL]: null });
  127 + },
  128 + getPopupContainer: () => document.body,
  129 + };
  130 + },
  131 + colProps: useGridLayout(2, 2, 2, 2, 2, 2) as unknown as ColEx,
  132 + },
  133 + {
  134 + field: SchemaFiled.AGG,
  135 + label: '数据聚合功能',
  136 + component: 'Select',
  137 + componentProps: {
  138 + getPopupContainer: () => document.body,
  139 + options: [
  140 + { label: '最小值', value: AggregateDataEnum.MIN },
  141 + { label: '最大值', value: AggregateDataEnum.MAX },
  142 + { label: '平均值', value: AggregateDataEnum.AVG },
  143 + { label: '求和', value: AggregateDataEnum.SUM },
  144 + { label: '计数', value: AggregateDataEnum.COUNT },
  145 + { label: '空', value: AggregateDataEnum.NONE },
  146 + ],
  147 + },
  148 + },
  149 + {
  150 + field: SchemaFiled.INTERVAL,
  151 + label: '分组间隔',
  152 + component: 'Select',
  153 + dynamicRules: ({ model }) => {
  154 + return [
  155 + {
  156 + required: model[SchemaFiled.AGG] !== AggregateDataEnum.NONE,
  157 + message: '分组间隔为必填项',
  158 + type: 'number',
  159 + },
  160 + ];
  161 + },
  162 + ifShow({ values }) {
  163 + return values[SchemaFiled.AGG] !== AggregateDataEnum.NONE;
  164 + },
  165 + componentProps({ formModel, formActionType }) {
  166 + const options =
  167 + formModel[SchemaFiled.WAY] === QueryWay.LATEST
  168 + ? getPacketIntervalByValue(formModel[SchemaFiled.START_TS])
  169 + : getPacketIntervalByRange(formModel[SchemaFiled.DATE_RANGE]);
  170 + if (formModel[SchemaFiled.AGG] !== AggregateDataEnum.NONE) {
  171 + formActionType.setFieldsValue({ [SchemaFiled.LIMIT]: null });
  172 + }
  173 + return {
  174 + options,
  175 + getPopupContainer: () => document.body,
  176 + };
  177 + },
  178 + },
  179 + {
  180 + field: SchemaFiled.LIMIT,
  181 + label: '最大条数',
  182 + component: 'InputNumber',
  183 + ifShow({ values }) {
  184 + return values[SchemaFiled.AGG] === AggregateDataEnum.NONE;
  185 + },
  186 + rules: [{ required: true, message: '最大条数为必填项' }],
  187 + helpMessage: ['根据查询条件,查出的数据条数不超过这个值'],
  188 + componentProps() {
  189 + return {
  190 + max: 50000,
  191 + min: 7,
  192 + getPopupContainer: () => document.body,
  193 + };
  194 + },
  195 + },
  196 + {
  197 + field: SchemaFiled.KEYS,
  198 + label: '设备属性',
  199 + component: 'Select',
  200 + componentProps: {
  201 + getPopupContainer: () => document.body,
  202 + },
  203 + },
  204 + ];
  205 +};
  206 +
  207 +export function getHistorySearchParams(value: Recordable) {
  208 + const { startTs, endTs, interval, agg, limit, way, keys, deviceId } = value;
  209 + if (way === QueryWay.LATEST) {
  210 + return {
  211 + keys,
  212 + entityId: deviceId,
  213 + startTs: moment().subtract(startTs, 'ms').valueOf(),
  214 + endTs: Date.now(),
  215 + interval,
  216 + agg,
  217 + limit,
  218 + };
  219 + } else {
  220 + return {
  221 + keys,
  222 + entityId: deviceId,
  223 + startTs: moment(startTs).valueOf(),
  224 + endTs: moment(endTs).valueOf(),
  225 + interval,
  226 + agg,
  227 + limit,
  228 + };
  229 + }
  230 +}
... ...
  1 +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys';
  2 +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type';
  3 +
  4 +const componentKeys = useComponentKeys('MapComponentTrackHistory');
  5 +
  6 +export const MapComponentTrackHistoryConfig: ConfigType = {
  7 + ...componentKeys,
  8 + title: '历史轨迹',
  9 + package: PackagesCategoryEnum.MAP,
  10 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentPropsConfigType } from '/@/views/visual/packages/index.type';
  3 + import { option } from './config';
  4 + import HistoryDataModel from './HistoryDataModal.vue';
  5 + import { useModal } from '/@/components/Modal';
  6 + import { Button, Tooltip, Spin } from 'ant-design-vue';
  7 + import {
  8 + PlayCircleOutlined,
  9 + PauseCircleOutlined,
  10 + ClockCircleOutlined,
  11 + } from '@ant-design/icons-vue';
  12 + import { computed, ref, toRaw, unref } from 'vue';
  13 + import { buildUUID } from '/@/utils/uuid';
  14 + import { useBaiduMapSDK } from '../../../hook/useBaiduMapSDK';
  15 + import { HistoryModalOkEmitParams, TrackAnimationStatus } from './type';
  16 + import { useMapTrackPlayback } from './useMapTrackPlayback';
  17 + import { shallowRef } from 'vue';
  18 + import { formatToDateTime } from '/@/utils/dateUtil';
  19 +
  20 + const props = defineProps<{
  21 + config: ComponentPropsConfigType<typeof option>;
  22 + }>();
  23 +
  24 + const wrapRef = ref();
  25 +
  26 + const wrapId = `bai-map-${buildUUID()}`;
  27 +
  28 + const mapInstance = shallowRef<Nullable<Recordable>>(null);
  29 +
  30 + const rangString = ref<Nullable<string>>(null);
  31 +
  32 + const [register, { openModal }] = useModal();
  33 +
  34 + const handleTrackSwitch = () => {
  35 + openModal(true, toRaw(props.config.option.dataSource));
  36 + };
  37 +
  38 + const getIsWidgetLibSelectMode = computed(() => {
  39 + return !props.config.option.dataSource;
  40 + });
  41 +
  42 + const handleRenderHistroyData = (data: HistoryModalOkEmitParams) => {
  43 + const { track, value } = data;
  44 + const { startTs, endTs } = value;
  45 + const formatType = 'YYYY-MM-DD HH:mm:ss';
  46 + rangString.value = `从${formatToDateTime(startTs, formatType)} 到 ${formatToDateTime(
  47 + endTs,
  48 + formatType
  49 + )}`;
  50 + genTrackPlaybackAnimation(track as any[]);
  51 + };
  52 +
  53 + const handlePlay = () => {
  54 + if (unref(playStatus) === TrackAnimationStatus.PAUSE) {
  55 + unref(continueFn)?.();
  56 + return;
  57 + }
  58 + if (unref(playStatus) === TrackAnimationStatus.DONE) {
  59 + unref(playFn)?.();
  60 + return;
  61 + }
  62 + if (unref(playStatus) === TrackAnimationStatus.PLAY) {
  63 + unref(pauseFn)?.();
  64 + return;
  65 + }
  66 + };
  67 +
  68 + async function initMap() {
  69 + const wrapEl = unref(wrapRef);
  70 + if (!wrapEl) return;
  71 + const BMapGL = (window as any).BMapGL;
  72 + if (!Reflect.has(window, 'BMapGL')) return;
  73 + mapInstance.value = new BMapGL.Map(wrapId);
  74 +
  75 + // 定位当前城市
  76 + const localcity = new BMapGL.LocalCity();
  77 + localcity.get(
  78 + (e: { center: Record<'lat' | 'lng', number>; code: number; level: number; name: string }) => {
  79 + const { center } = e;
  80 + const { lat, lng } = center;
  81 + const point = new BMapGL.Point(lng, lat);
  82 + unref(mapInstance)!.centerAndZoom(point, 15);
  83 + }
  84 + );
  85 +
  86 + unref(mapInstance)!.enableScrollWheelZoom(true);
  87 +
  88 + unref(getIsWidgetLibSelectMode) && genTrackPlaybackAnimation();
  89 + }
  90 +
  91 + const { loading } = useBaiduMapSDK(initMap);
  92 +
  93 + const { genTrackPlaybackAnimation, playStatus, playFn, continueFn, pauseFn } =
  94 + useMapTrackPlayback(mapInstance);
  95 +</script>
  96 +
  97 +<template>
  98 + <main class="w-full h-full flex flex-col justify-center items-center">
  99 + <div class="w-full flex justify-end">
  100 + <Button
  101 + type="text"
  102 + class="!px-2 flex-auto !text-left truncate mr-2"
  103 + :disabled="getIsWidgetLibSelectMode"
  104 + @click="handleTrackSwitch"
  105 + >
  106 + <div class="w-full truncate text-gray-500 flex items-center">
  107 + <ClockCircleOutlined class="mx-1" />
  108 + <span>历史</span>
  109 + <span class="mx-1">-</span>
  110 + <Tooltip :title="rangString || '请选择'">
  111 + <span class="truncate"> {{ rangString || '请选择' }} </span>
  112 + </Tooltip>
  113 + </div>
  114 + </Button>
  115 + <Button type="text" class="!px-2 !text-gray-500" @click="handlePlay">
  116 + <PlayCircleOutlined v-show="playStatus !== TrackAnimationStatus.PLAY" />
  117 + <PauseCircleOutlined class="!ml-0" v-show="playStatus === TrackAnimationStatus.PLAY" />
  118 + <span>
  119 + {{ playStatus !== TrackAnimationStatus.PLAY ? '播放轨迹' : '暂停播放' }}
  120 + </span>
  121 + </Button>
  122 + </div>
  123 + <Spin
  124 + :spinning="loading"
  125 + wrapper-class-name="map-spin-wrapper !w-full !h-full !flex justify-center items-center pointer-events-none"
  126 + tip="地图加载中..."
  127 + >
  128 + <div ref="wrapRef" :id="wrapId" class="w-full h-full no-drag"> </div>
  129 + </Spin>
  130 +
  131 + <HistoryDataModel @register="register" @ok="handleRenderHistroyData" />
  132 + </main>
  133 +</template>
  134 +
  135 +<style lang="less" scoped>
  136 + .map-spin-wrapper {
  137 + :deep(.ant-spin-container) {
  138 + @apply justify-center items-center p-2 w-full h-full;
  139 + }
  140 + }
  141 +</style>
... ...
  1 +import { SchemaFiled } from './history.config';
  2 +import { DataSource } from '/@/views/visual/palette/types';
  3 +
  4 +export interface HistoryModalParams {
  5 + dataSource?: DataSource[];
  6 +}
  7 +
  8 +export interface HistoryModalOkEmitParams {
  9 + track: {
  10 + lng: number | string;
  11 + lat: number | string;
  12 + }[];
  13 + value: Record<SchemaFiled, string | number>;
  14 +}
  15 +
  16 +export enum TrackAnimationStatus {
  17 + /**
  18 + * @description 播放中的状态
  19 + */
  20 + PLAY = 1,
  21 +
  22 + /**
  23 + * @description 完成和未开始的状态
  24 + */
  25 + DONE = 2,
  26 +
  27 + /**
  28 + * @description 暂停时的状态
  29 + */
  30 + PAUSE = 3,
  31 +}
... ...
  1 +import { ShallowRef, computed, ref, shallowRef, unref } from 'vue';
  2 +import { TrackAnimationStatus } from './type';
  3 +import { useTimeoutFn } from '@vueuse/core';
  4 +import { useMessage } from '/@/hooks/web/useMessage';
  5 +
  6 +export const useMapTrackPlayback = (mapInstance: ShallowRef<Nullable<Recordable>>) => {
  7 + const { createMessage } = useMessage();
  8 +
  9 + const trackAni = ref<Nullable<Recordable>>(null);
  10 +
  11 + const playFn = shallowRef<Nullable<Fn>>(() => {
  12 + trackAni.value?.start?.();
  13 + });
  14 +
  15 + const pauseFn = shallowRef<Nullable<Fn>>(() => {
  16 + trackAni.value?.pause?.();
  17 + });
  18 +
  19 + const continueFn = shallowRef<Nullable<Fn>>(() => {
  20 + trackAni.value?.continue?.();
  21 + });
  22 +
  23 + const playStatus = computed<TrackAnimationStatus>(() => {
  24 + return unref(trackAni)?._status;
  25 + });
  26 +
  27 + const RANDOM_PATH = [
  28 + {
  29 + lng: 116.297611,
  30 + lat: 40.047363,
  31 + },
  32 + {
  33 + lng: 116.302839,
  34 + lat: 40.048219,
  35 + },
  36 + {
  37 + lng: 116.308301,
  38 + lat: 40.050566,
  39 + },
  40 + {
  41 + lng: 116.305732,
  42 + lat: 40.054957,
  43 + },
  44 + {
  45 + lng: 116.304754,
  46 + lat: 40.057953,
  47 + },
  48 + {
  49 + lng: 116.306487,
  50 + lat: 40.058312,
  51 + },
  52 + {
  53 + lng: 116.307223,
  54 + lat: 40.056379,
  55 + },
  56 + ];
  57 +
  58 + const getAnimationDurationTime = (length: number) => {
  59 + const itemDurationTime = 1000;
  60 + const minDurationTime = 5000;
  61 + const durationTime = length * itemDurationTime;
  62 + return Math.max(durationTime, minDurationTime);
  63 + };
  64 +
  65 + /**
  66 + * @demo https://lbsyun.baidu.com/jsdemo.htm#fAnimationPause
  67 + * @param path
  68 + * @param clearOverlays
  69 + */
  70 + const genTrackPlaybackAnimation = (
  71 + path?: Record<'lng' | 'lat', number>[],
  72 + clearOverlays = true
  73 + ) => {
  74 + path = path || RANDOM_PATH;
  75 +
  76 + if (!path.length) {
  77 + createMessage.warning('无可用数据~');
  78 + return;
  79 + }
  80 +
  81 + const point: any[] = [];
  82 + const BMapGL = (window as any).BMapGL;
  83 +
  84 + clearOverlays && unref(mapInstance)?.clearOverlays();
  85 +
  86 + for (const { lng, lat } of path) {
  87 + point.push(new BMapGL.Point(lng, lat));
  88 + }
  89 +
  90 + const pl = new BMapGL.Polyline(point);
  91 + const BMapGLLib = (window as any).BMapGLLib;
  92 +
  93 + for (const item of path) {
  94 + marketPoint(item);
  95 + }
  96 +
  97 + const duration = getAnimationDurationTime(path?.length || 0);
  98 + const delay = 300;
  99 + trackAni.value = new BMapGLLib.TrackAnimation(unref(mapInstance), pl, {
  100 + overallView: true,
  101 + tilt: 30,
  102 + duration,
  103 + delay,
  104 + });
  105 +
  106 + useTimeoutFn(() => {
  107 + unref(playFn)?.();
  108 + }, 500);
  109 + };
  110 +
  111 + function marketPoint(params: Partial<Record<'lng' | 'lat', number>>) {
  112 + const { lng, lat } = params;
  113 + if (![lng, lat].every(Boolean)) return;
  114 + const BMap = (window as any).BMapGL;
  115 + const marker = new BMap.Marker(new BMap.Point(lng, lat));
  116 + unref(mapInstance)?.centerAndZoom(new BMap.Point(lng, lat));
  117 + unref(mapInstance)?.addOverlay(marker);
  118 + }
  119 +
  120 + return { genTrackPlaybackAnimation, playFn, pauseFn, continueFn, playStatus };
  121 +};
... ...
  1 +import cloneDeep from 'lodash-es/cloneDeep';
  2 +import { MapComponentTrackRealConfig } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '/@/views/visual/packages/index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig';
  10 +
  11 +export const option: PublicPresetOptions = {
  12 + componetDesign: false,
  13 + multipleDataSourceComponent: true,
  14 +};
  15 +
  16 +export default class Config extends PublicConfigClass implements CreateComponentType {
  17 + public key: string = MapComponentTrackRealConfig.key;
  18 +
  19 + public attr = { ...componentInitConfig };
  20 +
  21 + public componentConfig: ConfigType = cloneDeep(MapComponentTrackRealConfig);
  22 +
  23 + public persetOption = cloneDeep(option);
  24 +
  25 + public option: PublicComponentOptions;
  26 +
  27 + constructor(option: PublicComponentOptions) {
  28 + super();
  29 + this.option = { ...option };
  30 + }
  31 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  3 + import { useForm, BasicForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 +
  6 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  7 + schemas: [
  8 + {
  9 + field: ComponentConfigFieldEnum.FONT_COLOR,
  10 + label: '数值字体颜色',
  11 + component: 'ColorPicker',
  12 + changeEvent: 'update:value',
  13 + componentProps: {
  14 + defaultValue: '#FD7347',
  15 + },
  16 + },
  17 + {
  18 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  19 + label: '显示设备名称',
  20 + component: 'Checkbox',
  21 + },
  22 + ],
  23 + showActionButtonGroup: false,
  24 + labelWidth: 120,
  25 + baseColProps: {
  26 + span: 12,
  27 + },
  28 + });
  29 +
  30 + const getFormValues = () => {
  31 + return getFieldsValue();
  32 + };
  33 +
  34 + const setFormValues = (data: Recordable) => {
  35 + return setFieldsValue(data);
  36 + };
  37 +
  38 + defineExpose({
  39 + getFormValues,
  40 + setFormValues,
  41 + resetFormValues: resetFields,
  42 + } as PublicFormInstaceType);
  43 +</script>
  44 +
  45 +<template>
  46 + <BasicForm @register="register" />
  47 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { CreateComponentType } from '/@/views/visual/packages/index.type';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import {
  5 + PublicComponentValueType,
  6 + PublicFormInstaceType,
  7 + } from '/@/views/visual/dataSourceBindPanel/index.type';
  8 + import { commonDataSourceSchemas } from '../../../config/common.config';
  9 +
  10 + defineProps<{
  11 + values: PublicComponentValueType;
  12 + componentConfig: CreateComponentType;
  13 + }>();
  14 +
  15 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  16 + labelWidth: 0,
  17 + showActionButtonGroup: false,
  18 + layout: 'horizontal',
  19 + labelCol: { span: 0 },
  20 + schemas: commonDataSourceSchemas(),
  21 + });
  22 +
  23 + const getFormValues = () => {
  24 + return getFieldsValue();
  25 + };
  26 +
  27 + const setFormValues = (record: Recordable) => {
  28 + return setFieldsValue(record);
  29 + };
  30 +
  31 + defineExpose({
  32 + getFormValues,
  33 + setFormValues,
  34 + validate,
  35 + resetFormValues: resetFields,
  36 + } as PublicFormInstaceType);
  37 +</script>
  38 +
  39 +<template>
  40 + <BasicForm @register="register" />
  41 +</template>
... ...
  1 +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys';
  2 +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type';
  3 +
  4 +const componentKeys = useComponentKeys('MapComponentTrackReal');
  5 +
  6 +export const MapComponentTrackRealConfig: ConfigType = {
  7 + ...componentKeys,
  8 + title: '实时轨迹',
  9 + package: PackagesCategoryEnum.MAP,
  10 +};
... ...
  1 +<script lang="ts" setup>
  2 + import {
  3 + ComponentPropsConfigType,
  4 + MultipleDataFetchUpdateFn,
  5 + } from '/@/views/visual/packages/index.type';
  6 + import { option } from './config';
  7 + import { useMultipleDataFetch } from '/@/views/visual/packages/hook/useSocket';
  8 + import { useBaiduMapSDK } from '../../../hook/useBaiduMapSDK';
  9 + import { ref, unref } from 'vue';
  10 + import { buildUUID } from '/@/utils/uuid';
  11 + import { Spin } from 'ant-design-vue';
  12 + import { computed } from 'vue';
  13 + import { useMapTrackPlayBack } from './useMapTrackPlayback';
  14 +
  15 + const props = defineProps<{
  16 + config: ComponentPropsConfigType<typeof option>;
  17 + }>();
  18 +
  19 + const wrapRef = ref<Nullable<Recordable>>(null);
  20 +
  21 + const mapInstance = ref<Nullable<Recordable>>(null);
  22 +
  23 + const wrapId = `bai-map-${buildUUID()}`;
  24 +
  25 + /**
  26 + * @description 经度key
  27 + */
  28 + const getLngKey = computed(() => {
  29 + return props.config.option.dataSource?.at(0)?.attribute || '';
  30 + });
  31 +
  32 + /**
  33 + * @description 纬度key
  34 + */
  35 + const getLatKey = computed(() => {
  36 + return props.config.option.dataSource?.at(1)?.attribute || '';
  37 + });
  38 +
  39 + const validEffective = (value = '') => {
  40 + return !!(value && !isNaN(value as unknown as number));
  41 + };
  42 +
  43 + const updateFn: MultipleDataFetchUpdateFn = (message) => {
  44 + const { data = {} } = message;
  45 +
  46 + const lngData = data[unref(getLngKey)] || [];
  47 + const [lngLatest] = lngData;
  48 + const [, lng] = lngLatest;
  49 +
  50 + const latData = data[unref(getLatKey)] || [];
  51 + const [latLatest] = latData;
  52 + const [, lat] = latLatest;
  53 +
  54 + if (validEffective(lng) && validEffective(lat)) {
  55 + drawLine({ lng: Number(lng), lat: Number(lat) });
  56 + }
  57 + };
  58 +
  59 + useMultipleDataFetch(props, updateFn);
  60 +
  61 + const { drawLine } = useMapTrackPlayBack(mapInstance);
  62 +
  63 + const initMap = () => {
  64 + const wrapEl = unref(wrapRef);
  65 + if (!wrapEl) return;
  66 + if (!Reflect.has(window, 'BMapGL')) return;
  67 + const BMapGL = (window as any).BMapGL;
  68 + mapInstance.value = new BMapGL.Map(wrapId);
  69 +
  70 + // 定位当前城市
  71 + const localcity = new BMapGL.LocalCity();
  72 + localcity.get(
  73 + (e: { center: Record<'lat' | 'lng', number>; code: number; level: number; name: string }) => {
  74 + const { center } = e;
  75 + const { lat, lng } = center;
  76 + const point = new BMapGL.Point(lng, lat);
  77 + unref(mapInstance)!.centerAndZoom(point, 15);
  78 + }
  79 + );
  80 +
  81 + unref(mapInstance)!.enableScrollWheelZoom(true);
  82 + };
  83 +
  84 + const { loading } = useBaiduMapSDK(initMap);
  85 +</script>
  86 +
  87 +<template>
  88 + <main class="w-full h-full flex flex-col p-2 justify-center items-center">
  89 + <Spin
  90 + :spinning="loading"
  91 + wrapper-class-name="map-spin-wrapper !w-full !h-full !flex justify-center items-center pointer-events-none"
  92 + tip="地图加载中..."
  93 + />
  94 + <div v-show="!loading" ref="wrapRef" :id="wrapId" class="w-full h-full no-drag"> </div>
  95 + </main>
  96 +</template>
  97 +
  98 +<style lang="less" scoped>
  99 + // .map-spin-wrapper {
  100 + // :deep(.ant-spin-container) {
  101 + // @apply justify-center items-center p-2 w-full h-full;
  102 + // }
  103 + // }
  104 +</style>
... ...
  1 +import { Ref, unref } from 'vue';
  2 +
  3 +export const useMapTrackPlayBack = (mapInstance: Ref<Nullable<Recordable>>) => {
  4 + const positionList: Record<'lng' | 'lat', number>[] = [];
  5 +
  6 + const drawLine = (position: Record<'lng' | 'lat', number>) => {
  7 + try {
  8 + if (!position.lat || !position.lng) return;
  9 + positionList.push(position);
  10 + marketPoint(position);
  11 + if (positionList.length === 1) return;
  12 +
  13 + const point: any[] = [];
  14 + const BMapGL = (window as any).BMapGL;
  15 +
  16 + for (const { lng, lat } of positionList.slice(-2)) {
  17 + point.push(new BMapGL.Point(lng, lat));
  18 + }
  19 +
  20 + const pl = new BMapGL.Polyline(point);
  21 + const BMapGLLib = (window as any).BMapGLLib;
  22 +
  23 + const duration = 2000;
  24 + const delay = 300;
  25 + const trackAni = new BMapGLLib.TrackAnimation(unref(mapInstance), pl, {
  26 + overallView: true,
  27 + tilt: 30,
  28 + duration,
  29 + delay,
  30 + });
  31 + trackAni?.start?.();
  32 + } catch (error) {
  33 + throw error;
  34 + }
  35 + };
  36 +
  37 + function marketPoint(params: Partial<Record<'lng' | 'lat', number>>) {
  38 + const { lng, lat } = params;
  39 + if (![lng, lat].every(Boolean)) return;
  40 + const BMap = (window as any).BMapGL;
  41 + if (!BMap) return;
  42 + const marker = new BMap.Marker(new BMap.Point(lng, lat));
  43 + unref(mapInstance)?.centerAndZoom(new BMap.Point(lng, lat));
  44 + unref(mapInstance)?.addOverlay(marker);
  45 + }
  46 +
  47 + return { drawLine };
  48 +};
... ...
  1 +import { MapComponentTrackHistoryConfig } from './MapComponentTrackHistory';
  2 +import { MapComponentTrackRealConfig } from './MapComponentTrackReal';
  3 +
  4 +export const MapList = [MapComponentTrackHistoryConfig, MapComponentTrackRealConfig];
... ...
  1 +import { cloneDeep } from 'lodash-es';
  2 +import { Picture } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '../../../index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '../../../publicConfig';
  10 +
  11 +export const option: PublicPresetOptions = {
  12 + componetDesign: false,
  13 +};
  14 +
  15 +export default class Config extends PublicConfigClass implements CreateComponentType {
  16 + public key: string = Picture.key;
  17 +
  18 + public attr = { ...componentInitConfig };
  19 +
  20 + public componentConfig: ConfigType = cloneDeep(Picture);
  21 +
  22 + public persetOption = cloneDeep(option);
  23 +
  24 + public option: PublicComponentOptions;
  25 +
  26 + constructor(option: PublicComponentOptions) {
  27 + super();
  28 + this.option = { ...option };
  29 + }
  30 +}
... ...
  1 +<script lang="ts" setup></script>
  2 +
  3 +<template><main></main></template>
... ...
  1 +<script lang="ts" setup>
  2 + // import { computed } from 'vue';
  3 + import { commonDataSourceSchemas } from '../../../config/common.config';
  4 + import { BasicForm, useForm } from '/@/components/Form';
  5 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  6 +
  7 + // const props = defineProps<{
  8 + // values: PublicComponentDataSourceProps;
  9 + // }>();
  10 +
  11 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  12 + labelWidth: 0,
  13 + showActionButtonGroup: false,
  14 + layout: 'horizontal',
  15 + labelCol: { span: 0 },
  16 + schemas: commonDataSourceSchemas(),
  17 + });
  18 +
  19 + // const getBindValues = computed(() => {
  20 + // return props.values.bindValue;
  21 + // });
  22 +
  23 + const getFormValues = () => {
  24 + return getFieldsValue();
  25 + };
  26 +
  27 + const setFormValues = (record: Recordable) => {
  28 + return setFieldsValue(record);
  29 + };
  30 +
  31 + defineExpose({
  32 + getFormValues,
  33 + setFormValues,
  34 + validate,
  35 + resetFormValues: resetFields,
  36 + } as PublicFormInstaceType);
  37 +</script>
  38 +
  39 +<template>
  40 + <BasicForm @register="register" />
  41 +</template>
... ...
  1 +import { useComponentKeys } from '../../../hook/useComponentKeys';
  2 +import { ConfigType, PackagesCategoryEnum } from '../../../index.type';
  3 +
  4 +const componentKeys = useComponentKeys('Picture');
  5 +export const Picture: ConfigType = {
  6 + ...componentKeys,
  7 + title: '图片组件',
  8 + package: PackagesCategoryEnum.PICTURE,
  9 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { ref } from 'vue';
  3 + import { Image as AntImage } from 'ant-design-vue';
  4 + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime';
  5 + import { useDataFetch } from '../../../hook/useSocket';
  6 + import { ComponentPropsConfigType, DataFetchUpdateFn } from '../../../index.type';
  7 +
  8 + const props = defineProps<{
  9 + config: ComponentPropsConfigType;
  10 + }>();
  11 +
  12 + const fallback =
  13 + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg==';
  14 +
  15 + const time = ref<Nullable<number>>(null);
  16 +
  17 + const url = ref<string>(fallback);
  18 +
  19 + // const getImagBase64 = ref(fallback);
  20 +
  21 + // const getBase64Image = (url: string) => {
  22 + // let canvas: Nullable<HTMLCanvasElement> = document.createElement('canvas');
  23 + // const ctx = canvas.getContext('2d');
  24 + // let image: Nullable<HTMLImageElement> = new Image();
  25 +
  26 + // image.onload = function () {
  27 + // canvas!.height = image!.height;
  28 + // canvas!.width = image!.width;
  29 + // ctx?.drawImage(image!, 0, 0);
  30 + // const dataUrl = canvas!.toDataURL('image/png');
  31 + // getImagBase64.value = dataUrl;
  32 + // console.log(dataUrl);
  33 + // image = null;
  34 + // canvas = null;
  35 + // };
  36 + // image.setAttribute('crossOrigin', 'Anonymous');
  37 + // image.src = url;
  38 + // };
  39 +
  40 + const updateFn: DataFetchUpdateFn = (message, attribute) => {
  41 + const { data = {} } = message;
  42 + const [latest] = data[attribute] || [];
  43 + const [timespan, value] = latest;
  44 + time.value = timespan;
  45 + url.value = `${value}?timespan=${timespan}`;
  46 + };
  47 +
  48 + useDataFetch(props, updateFn);
  49 +</script>
  50 +
  51 +<template>
  52 + <main class="w-full h-full flex flex-col items-center">
  53 + <AntImage :src="url" :fallback="fallback" />
  54 + <UpdateTime :time="time" />
  55 + </main>
  56 +</template>
  57 +
  58 +<style scoped lang="less">
  59 + :deep(.ant-image) {
  60 + @apply flex justify-center w-auto;
  61 +
  62 + height: calc(100% - 56px);
  63 +
  64 + img {
  65 + @apply w-auto h-full;
  66 + }
  67 + }
  68 +</style>
... ...
  1 +import { Picture } from './Picture';
  2 +
  3 +export enum ComponentCategoryEnum {
  4 + PICTURE = 'Picture',
  5 +}
  6 +
  7 +export enum ComponentCategoryNameEnum {
  8 + PICTURE = '图片组件',
  9 +}
  10 +
  11 +export const PictureList = [Picture];
... ...
  1 +import cloneDeep from 'lodash-es/cloneDeep';
  2 +import { TextComponent1Config } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '/@/views/visual/packages/index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig';
  10 +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  11 +
  12 +export const option: PublicPresetOptions = {
  13 + [ComponentConfigFieldEnum.FONT_COLOR]: '#000',
  14 + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false,
  15 +};
  16 +
  17 +export default class Config extends PublicConfigClass implements CreateComponentType {
  18 + public key: string = TextComponent1Config.key;
  19 +
  20 + public attr = { ...componentInitConfig };
  21 +
  22 + public componentConfig: ConfigType = cloneDeep(TextComponent1Config);
  23 +
  24 + public persetOption = cloneDeep(option);
  25 +
  26 + public option: PublicComponentOptions;
  27 +
  28 + constructor(option: PublicComponentOptions) {
  29 + super();
  30 + this.option = { ...option };
  31 + }
  32 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  3 + import { useForm, BasicForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 +
  6 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  7 + schemas: [
  8 + {
  9 + field: ComponentConfigFieldEnum.FONT_COLOR,
  10 + label: '数值字体颜色',
  11 + component: 'ColorPicker',
  12 + changeEvent: 'update:value',
  13 + componentProps: {
  14 + defaultValue: '#000',
  15 + },
  16 + },
  17 + {
  18 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  19 + label: '显示设备名称',
  20 + component: 'Checkbox',
  21 + },
  22 + ],
  23 + showActionButtonGroup: false,
  24 + labelWidth: 120,
  25 + baseColProps: {
  26 + span: 12,
  27 + },
  28 + });
  29 +
  30 + const getFormValues = () => {
  31 + return getFieldsValue();
  32 + };
  33 +
  34 + const setFormValues = (data: Recordable) => {
  35 + return setFieldsValue(data);
  36 + };
  37 +
  38 + defineExpose({
  39 + getFormValues,
  40 + setFormValues,
  41 + resetFormValues: resetFields,
  42 + } as PublicFormInstaceType);
  43 +</script>
  44 +
  45 +<template>
  46 + <BasicForm @register="register" />
  47 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { commonDataSourceSchemas } from '../../../config/common.config';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 +
  6 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  7 + labelWidth: 0,
  8 + showActionButtonGroup: false,
  9 + layout: 'horizontal',
  10 + labelCol: { span: 0 },
  11 + schemas: commonDataSourceSchemas(),
  12 + });
  13 +
  14 + const getFormValues = () => {
  15 + return getFieldsValue();
  16 + };
  17 +
  18 + const setFormValues = (record: Recordable) => {
  19 + return setFieldsValue(record);
  20 + };
  21 +
  22 + defineExpose({
  23 + getFormValues,
  24 + setFormValues,
  25 + validate,
  26 + resetFormValues: resetFields,
  27 + } as PublicFormInstaceType);
  28 +</script>
  29 +
  30 +<template>
  31 + <BasicForm @register="register" />
  32 +</template>
... ...
  1 +// import { ComponentEnum, ComponentNameEnum } from '../index.type';
  2 +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys';
  3 +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type';
  4 +
  5 +const componentKeys = useComponentKeys('TextComponent1');
  6 +export const TextComponent1Config: ConfigType = {
  7 + ...componentKeys,
  8 + // title: ComponentNameEnum.TEXT_COMPONENT_1,
  9 + title: '文本组件1',
  10 + package: PackagesCategoryEnum.TEXT,
  11 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type';
  3 + import { option } from './config';
  4 + import { useComponentScale } from '/@/views/visual/packages/hook/useComponentScale';
  5 + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket';
  6 + import { computed } from 'vue';
  7 + import { ref } from 'vue';
  8 + import { DeviceName } from '/@/views/visual/commonComponents/DeviceName';
  9 +
  10 + const props = defineProps<{
  11 + config: ComponentPropsConfigType<typeof option>;
  12 + }>();
  13 +
  14 + const currentValue = ref<string | number>(123);
  15 +
  16 + const getDesign = computed(() => {
  17 + const { persetOption = {}, option } = props.config;
  18 +
  19 + const { fontColor: persetFontColor } = persetOption;
  20 +
  21 + const { componentInfo, attribute, attributeRename } = option;
  22 +
  23 + const { fontColor } = componentInfo || {};
  24 + return {
  25 + fontColor: fontColor || persetFontColor,
  26 + attribute: attributeRename || attribute,
  27 + };
  28 + });
  29 +
  30 + const updateFn: DataFetchUpdateFn = (message, attribute) => {
  31 + const { data = {} } = message;
  32 + const [latest] = data[attribute] || [];
  33 + const [_, value] = latest;
  34 + currentValue.value = value;
  35 + };
  36 +
  37 + useDataFetch(props, updateFn);
  38 +
  39 + const { getScale } = useComponentScale(props);
  40 +</script>
  41 +
  42 +<template>
  43 + <main :style="getScale" class="w-full h-full flex flex-col justify-center items-center">
  44 + <DeviceName :config="config" />
  45 +
  46 + <h1 class="my-4 font-bold text-xl !my-2 truncate" :style="{ color: getDesign.fontColor }">
  47 + {{ currentValue || 0 }}
  48 + </h1>
  49 + <div class="text-gray-500 text-lg truncate">{{ getDesign.attribute || '温度' }}</div>
  50 + <!-- <div v-if="config.option.componentInfo.showDeviceName">
  51 + {{ config.option.deviceRename || config.option.deviceName }}
  52 + </div> -->
  53 + </main>
  54 +</template>
... ...
  1 +import cloneDeep from 'lodash-es/cloneDeep';
  2 +import { TextComponent2Config } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '/@/views/visual/packages/index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig';
  10 +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  11 +
  12 +export const option: PublicPresetOptions = {
  13 + [ComponentConfigFieldEnum.FONT_COLOR]: '#000',
  14 + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false,
  15 +};
  16 +
  17 +export default class Config extends PublicConfigClass implements CreateComponentType {
  18 + public key: string = TextComponent2Config.key;
  19 +
  20 + public attr = { ...componentInitConfig };
  21 +
  22 + public componentConfig: ConfigType = cloneDeep(TextComponent2Config);
  23 +
  24 + public persetOption = cloneDeep(option);
  25 +
  26 + public option: PublicComponentOptions;
  27 +
  28 + constructor(option: PublicComponentOptions) {
  29 + super();
  30 + this.option = { ...option };
  31 + }
  32 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  3 + import { useForm, BasicForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 +
  6 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  7 + schemas: [
  8 + {
  9 + field: ComponentConfigFieldEnum.FONT_COLOR,
  10 + label: '数值字体颜色',
  11 + component: 'ColorPicker',
  12 + changeEvent: 'update:value',
  13 + componentProps: {
  14 + defaultValue: '#000',
  15 + },
  16 + },
  17 + {
  18 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  19 + label: '显示设备名称',
  20 + component: 'Checkbox',
  21 + },
  22 + ],
  23 + showActionButtonGroup: false,
  24 + labelWidth: 120,
  25 + baseColProps: {
  26 + span: 12,
  27 + },
  28 + });
  29 +
  30 + const getFormValues = () => {
  31 + return getFieldsValue();
  32 + };
  33 +
  34 + const setFormValues = (data: Recordable) => {
  35 + return setFieldsValue(data);
  36 + };
  37 +
  38 + defineExpose({
  39 + getFormValues,
  40 + setFormValues,
  41 + resetFormValues: resetFields,
  42 + } as PublicFormInstaceType);
  43 +</script>
  44 +
  45 +<template>
  46 + <BasicForm @register="register" />
  47 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { commonDataSourceSchemas } from '../../../config/common.config';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 +
  6 + // const props = defineProps<{
  7 + // values: PublicComponentDataSourceProps;
  8 + // }>();
  9 +
  10 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  11 + labelWidth: 0,
  12 + showActionButtonGroup: false,
  13 + layout: 'horizontal',
  14 + labelCol: { span: 0 },
  15 + schemas: commonDataSourceSchemas(),
  16 + });
  17 +
  18 + const getFormValues = () => {
  19 + return getFieldsValue();
  20 + };
  21 +
  22 + const setFormValues = (record: Recordable) => {
  23 + return setFieldsValue(record);
  24 + };
  25 +
  26 + defineExpose({
  27 + getFormValues,
  28 + setFormValues,
  29 + validate,
  30 + resetFormValues: resetFields,
  31 + } as PublicFormInstaceType);
  32 +</script>
  33 +
  34 +<template>
  35 + <BasicForm @register="register" />
  36 +</template>
... ...
  1 +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys';
  2 +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type';
  3 +
  4 +const componentKeys = useComponentKeys('TextComponent2');
  5 +export const TextComponent2Config: ConfigType = {
  6 + ...componentKeys,
  7 + title: '文本组件2',
  8 + package: PackagesCategoryEnum.TEXT,
  9 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type';
  3 + import { option } from './config';
  4 + import { useComponentScale } from '/@/views/visual/packages/hook/useComponentScale';
  5 + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket';
  6 + import { computed } from 'vue';
  7 + import { ref } from 'vue';
  8 + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime';
  9 + import { DeviceName } from '/@/views/visual/commonComponents/DeviceName';
  10 +
  11 + const props = defineProps<{
  12 + config: ComponentPropsConfigType<typeof option>;
  13 + }>();
  14 +
  15 + const currentValue = ref<string | number>(123);
  16 + const time = ref<Nullable<number>>(null);
  17 +
  18 + const getDesign = computed(() => {
  19 + const { persetOption = {}, option } = props.config;
  20 +
  21 + const { fontColor: persetFontColor } = persetOption;
  22 +
  23 + const { componentInfo, attribute, attributeRename } = option;
  24 +
  25 + const { fontColor } = componentInfo || {};
  26 +
  27 + return {
  28 + fontColor: fontColor || persetFontColor,
  29 + attribute: attributeRename || attribute,
  30 + };
  31 + });
  32 +
  33 + const updateFn: DataFetchUpdateFn = (message, attribute) => {
  34 + const { data = {} } = message;
  35 + const [info] = data[attribute] || [];
  36 + const [timespan, value] = info;
  37 + currentValue.value = value;
  38 + time.value = timespan;
  39 + };
  40 +
  41 + useDataFetch(props, updateFn);
  42 +
  43 + const { getScale } = useComponentScale(props);
  44 +</script>
  45 +
  46 +<template>
  47 + <main class="w-full h-full flex flex-col justify-center items-center">
  48 + <DeviceName :config="config" />
  49 +
  50 + <div class="flex-1 flex justify-center items-center flex-col" :style="getScale">
  51 + <h1 class="my-4 font-bold text-xl !my-2 truncate" :style="{ color: getDesign.fontColor }">
  52 + {{ currentValue || 0 }}
  53 + </h1>
  54 + <div class="text-gray-500 text-lg truncate">{{ getDesign.attribute || '温度' }}</div>
  55 + </div>
  56 + <UpdateTime :time="time" />
  57 + </main>
  58 +</template>
... ...
  1 +import cloneDeep from 'lodash-es/cloneDeep';
  2 +import { TextComponent3Config } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '/@/views/visual/packages/index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig';
  10 +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  11 +
  12 +export const option: PublicPresetOptions = {
  13 + [ComponentConfigFieldEnum.UNIT]: '℃',
  14 + [ComponentConfigFieldEnum.ICON]: 'shuiwen',
  15 + [ComponentConfigFieldEnum.ICON_COLOR]: '#367bff',
  16 + [ComponentConfigFieldEnum.FONT_COLOR]: '#000',
  17 + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false,
  18 +};
  19 +
  20 +export default class Config extends PublicConfigClass implements CreateComponentType {
  21 + public key: string = TextComponent3Config.key;
  22 +
  23 + public attr = { ...componentInitConfig };
  24 +
  25 + public componentConfig: ConfigType = cloneDeep(TextComponent3Config);
  26 +
  27 + public persetOption = cloneDeep(option);
  28 +
  29 + public option: PublicComponentOptions;
  30 +
  31 + constructor(option: PublicComponentOptions) {
  32 + super();
  33 + this.option = { ...option };
  34 + }
  35 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  3 + import { useForm, BasicForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 + import { option } from './config';
  6 +
  7 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  8 + schemas: [
  9 + {
  10 + field: ComponentConfigFieldEnum.FONT_COLOR,
  11 + label: '数值字体颜色',
  12 + component: 'ColorPicker',
  13 + changeEvent: 'update:value',
  14 + valueField: 'value',
  15 + defaultValue: option.fontColor,
  16 + },
  17 + {
  18 + field: ComponentConfigFieldEnum.UNIT,
  19 + label: '数值单位',
  20 + component: 'Input',
  21 + defaultValue: option.unit,
  22 + componentProps: {
  23 + placeholder: '请输入数值单位',
  24 + },
  25 + },
  26 + {
  27 + field: ComponentConfigFieldEnum.ICON_COLOR,
  28 + label: '图标颜色',
  29 + component: 'ColorPicker',
  30 + changeEvent: 'update:value',
  31 + defaultValue: option.iconColor,
  32 + },
  33 + {
  34 + field: ComponentConfigFieldEnum.ICON,
  35 + label: '图标',
  36 + component: 'IconDrawer',
  37 + changeEvent: 'update:value',
  38 + defaultValue: option.icon,
  39 + componentProps({ formModel }) {
  40 + const color = formModel[ComponentConfigFieldEnum.ICON_COLOR];
  41 + return {
  42 + color,
  43 + };
  44 + },
  45 + },
  46 + {
  47 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  48 + label: '显示设备名称',
  49 + component: 'Checkbox',
  50 + defaultValue: option.showDeviceName,
  51 + },
  52 + ],
  53 + showActionButtonGroup: false,
  54 + labelWidth: 120,
  55 + baseColProps: {
  56 + span: 12,
  57 + },
  58 + });
  59 +
  60 + const getFormValues = () => {
  61 + return getFieldsValue();
  62 + };
  63 +
  64 + const setFormValues = (data: Recordable) => {
  65 + return setFieldsValue(data);
  66 + };
  67 +
  68 + defineExpose({
  69 + getFormValues,
  70 + setFormValues,
  71 + resetFormValues: resetFields,
  72 + } as PublicFormInstaceType);
  73 +</script>
  74 +
  75 +<template>
  76 + <BasicForm @register="register" />
  77 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { commonDataSourceSchemas } from '../../../config/common.config';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 +
  6 + // const props = defineProps<{
  7 + // values: PublicComponentDataSourceProps;
  8 + // }>();
  9 +
  10 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  11 + labelWidth: 0,
  12 + showActionButtonGroup: false,
  13 + layout: 'horizontal',
  14 + labelCol: { span: 0 },
  15 + schemas: commonDataSourceSchemas(),
  16 + });
  17 +
  18 + const getFormValues = () => {
  19 + return getFieldsValue();
  20 + };
  21 +
  22 + const setFormValues = (record: Recordable) => {
  23 + return setFieldsValue(record);
  24 + };
  25 +
  26 + defineExpose({
  27 + getFormValues,
  28 + setFormValues,
  29 + validate,
  30 + resetFormValues: resetFields,
  31 + } as PublicFormInstaceType);
  32 +</script>
  33 +
  34 +<template>
  35 + <BasicForm @register="register" />
  36 +</template>
... ...
  1 +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys';
  2 +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type';
  3 +
  4 +const componentKeys = useComponentKeys('TextComponent3');
  5 +export const TextComponent3Config: ConfigType = {
  6 + ...componentKeys,
  7 + title: '文本组件3',
  8 + package: PackagesCategoryEnum.TEXT,
  9 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type';
  3 + import { option } from './config';
  4 + import { useComponentScale } from '/@/views/visual/packages/hook/useComponentScale';
  5 + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket';
  6 + import { ref } from 'vue';
  7 + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime';
  8 + import { SvgIcon } from '/@/components/Icon';
  9 + import { computed } from 'vue';
  10 + import { DeviceName } from '/@/views/visual/commonComponents/DeviceName';
  11 +
  12 + const props = defineProps<{
  13 + config: ComponentPropsConfigType<typeof option>;
  14 + }>();
  15 +
  16 + const currentValue = ref<string | number>(123);
  17 +
  18 + const time = ref<Nullable<number>>(null);
  19 +
  20 + const getDesign = computed(() => {
  21 + const { persetOption = {}, option } = props.config;
  22 +
  23 + const {
  24 + iconColor: persetIconColor,
  25 + unit: perseUnit,
  26 + icon: persetIcon,
  27 + fontColor: persetFontColor,
  28 + } = persetOption;
  29 +
  30 + const { componentInfo, attribute, attributeRename } = option;
  31 +
  32 + const { icon, iconColor, fontColor, unit } = componentInfo || {};
  33 + return {
  34 + iconColor: iconColor || persetIconColor,
  35 + unit: unit ?? perseUnit,
  36 + icon: icon || persetIcon,
  37 + fontColor: fontColor || persetFontColor,
  38 + attribute: attributeRename || attribute,
  39 + };
  40 + });
  41 +
  42 + const updateFn: DataFetchUpdateFn = (message, attribute) => {
  43 + const { data = {} } = message;
  44 + const [latest] = data[attribute] || [];
  45 + const [timespan, value] = latest;
  46 + currentValue.value = value;
  47 + time.value = timespan;
  48 + };
  49 +
  50 + useDataFetch(props, updateFn);
  51 +
  52 + const { getScale } = useComponentScale(props);
  53 +</script>
  54 +
  55 +<template>
  56 + <main :style="getScale" class="w-full h-full flex flex-col justify-center items-center">
  57 + <DeviceName :config="config" />
  58 + <div class="flex-1 flex justify-center items-center flex-col">
  59 + <SvgIcon
  60 + :name="getDesign.icon!"
  61 + prefix="iconfont"
  62 + :size="60"
  63 + :style="{ color: getDesign.iconColor }"
  64 + />
  65 + <h1 class="font-bold text-xl m-2 truncate" :style="{ color: getDesign.fontColor }">
  66 + <span> {{ currentValue || 0 }}</span>
  67 + <span>{{ getDesign.unit }} </span>
  68 + </h1>
  69 + <div class="text-gray-500 text-lg truncate">{{ getDesign.attribute || '温度' }}</div>
  70 + </div>
  71 + <UpdateTime :time="time" />
  72 + </main>
  73 +</template>
... ...
  1 +import cloneDeep from 'lodash-es/cloneDeep';
  2 +import { TextComponent4Config } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '/@/views/visual/packages/index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig';
  10 +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  11 +
  12 +export const option: PublicPresetOptions = {
  13 + [ComponentConfigFieldEnum.UNIT]: '℃',
  14 + [ComponentConfigFieldEnum.ICON]: 'shuiwen',
  15 + [ComponentConfigFieldEnum.ICON_COLOR]: '#367bff',
  16 + [ComponentConfigFieldEnum.FONT_COLOR]: '#000',
  17 + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false,
  18 +};
  19 +
  20 +export default class Config extends PublicConfigClass implements CreateComponentType {
  21 + public key: string = TextComponent4Config.key;
  22 +
  23 + public attr = { ...componentInitConfig };
  24 +
  25 + public componentConfig: ConfigType = cloneDeep(TextComponent4Config);
  26 +
  27 + public persetOption = cloneDeep(option);
  28 +
  29 + public option: PublicComponentOptions;
  30 +
  31 + constructor(option: PublicComponentOptions) {
  32 + super();
  33 + this.option = { ...option };
  34 + }
  35 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  3 + import { useForm, BasicForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 + import { option } from './config';
  6 +
  7 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  8 + schemas: [
  9 + {
  10 + field: ComponentConfigFieldEnum.FONT_COLOR,
  11 + label: '数值字体颜色',
  12 + component: 'ColorPicker',
  13 + changeEvent: 'update:value',
  14 + valueField: 'value',
  15 + defaultValue: option.fontColor,
  16 + },
  17 + {
  18 + field: ComponentConfigFieldEnum.UNIT,
  19 + label: '数值单位',
  20 + component: 'Input',
  21 + defaultValue: option.unit,
  22 + componentProps: {
  23 + placeholder: '请输入数值单位',
  24 + },
  25 + },
  26 + {
  27 + field: ComponentConfigFieldEnum.ICON_COLOR,
  28 + label: '图标颜色',
  29 + component: 'ColorPicker',
  30 + changeEvent: 'update:value',
  31 + defaultValue: option.iconColor,
  32 + },
  33 + {
  34 + field: ComponentConfigFieldEnum.ICON,
  35 + label: '图标',
  36 + component: 'IconDrawer',
  37 + changeEvent: 'update:value',
  38 + defaultValue: option.icon,
  39 + componentProps({ formModel }) {
  40 + const color = formModel[ComponentConfigFieldEnum.ICON_COLOR];
  41 + return {
  42 + color,
  43 + };
  44 + },
  45 + },
  46 + {
  47 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  48 + label: '显示设备名称',
  49 + component: 'Checkbox',
  50 + defaultValue: option.showDeviceName,
  51 + },
  52 + ],
  53 + showActionButtonGroup: false,
  54 + labelWidth: 120,
  55 + baseColProps: {
  56 + span: 12,
  57 + },
  58 + });
  59 +
  60 + const getFormValues = () => {
  61 + return getFieldsValue();
  62 + };
  63 +
  64 + const setFormValues = (data: Recordable) => {
  65 + return setFieldsValue(data);
  66 + };
  67 +
  68 + defineExpose({
  69 + getFormValues,
  70 + setFormValues,
  71 + resetFormValues: resetFields,
  72 + } as PublicFormInstaceType);
  73 +</script>
  74 +
  75 +<template>
  76 + <BasicForm @register="register" />
  77 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { commonDataSourceSchemas } from '../../../config/common.config';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 +
  6 + // const props = defineProps<{
  7 + // values: PublicComponentDataSourceProps;
  8 + // }>();
  9 +
  10 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  11 + labelWidth: 0,
  12 + showActionButtonGroup: false,
  13 + layout: 'horizontal',
  14 + labelCol: { span: 0 },
  15 + schemas: commonDataSourceSchemas(),
  16 + });
  17 +
  18 + const getFormValues = () => {
  19 + return getFieldsValue();
  20 + };
  21 +
  22 + const setFormValues = (record: Recordable) => {
  23 + return setFieldsValue(record);
  24 + };
  25 +
  26 + defineExpose({
  27 + getFormValues,
  28 + setFormValues,
  29 + validate,
  30 + resetFormValues: resetFields,
  31 + } as PublicFormInstaceType);
  32 +</script>
  33 +
  34 +<template>
  35 + <BasicForm @register="register" />
  36 +</template>
... ...
  1 +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys';
  2 +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type';
  3 +
  4 +const componentKeys = useComponentKeys('TextComponent4');
  5 +export const TextComponent4Config: ConfigType = {
  6 + ...componentKeys,
  7 + title: '文本组件4',
  8 + package: PackagesCategoryEnum.TEXT,
  9 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type';
  3 + import { option } from './config';
  4 + import { useComponentScale } from '/@/views/visual/packages/hook/useComponentScale';
  5 + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket';
  6 + import { computed } from 'vue';
  7 + import { ref } from 'vue';
  8 + import { SvgIcon } from '/@/components/Icon';
  9 + import { DeviceName } from '/@/views/visual/commonComponents/DeviceName';
  10 +
  11 + const props = defineProps<{
  12 + config: ComponentPropsConfigType<typeof option>;
  13 + }>();
  14 +
  15 + const currentValue = ref<string | number>(123);
  16 +
  17 + const getDesign = computed(() => {
  18 + const { persetOption = {}, option } = props.config;
  19 +
  20 + const {
  21 + iconColor: persetIconColor,
  22 + unit: perseUnit,
  23 + icon: persetIcon,
  24 + fontColor: persetFontColor,
  25 + } = persetOption;
  26 +
  27 + const { componentInfo, attribute, attributeRename } = option;
  28 +
  29 + const { icon, iconColor, fontColor, unit } = componentInfo || {};
  30 + return {
  31 + iconColor: iconColor || persetIconColor,
  32 + unit: unit ?? perseUnit,
  33 + icon: icon || persetIcon,
  34 + fontColor: fontColor || persetFontColor,
  35 + attribute: attributeRename || attribute,
  36 + };
  37 + });
  38 +
  39 + const updateFn: DataFetchUpdateFn = (message, attribute) => {
  40 + const { data = {} } = message;
  41 + const [info] = data[attribute] || [];
  42 + const [_, value] = info;
  43 + currentValue.value = value;
  44 + };
  45 +
  46 + useDataFetch(props, updateFn);
  47 +
  48 + const { getScale } = useComponentScale(props);
  49 +</script>
  50 +
  51 +<template>
  52 + <main class="w-full h-full flex flex-col justify-center items-center">
  53 + <DeviceName :config="config" />
  54 + <div :style="getScale" class="flex-1 flex justify-center items-center flex-col">
  55 + <SvgIcon
  56 + :name="getDesign.icon!"
  57 + prefix="iconfont"
  58 + :size="60"
  59 + :style="{ color: getDesign.iconColor }"
  60 + />
  61 + <h1 class="my-4 font-bold text-lg !my-2 truncate" :style="{ color: getDesign.fontColor }">
  62 + <span>{{ currentValue || 0 }}</span>
  63 + <span>{{ getDesign.unit }}</span>
  64 + </h1>
  65 + <div class="text-gray-500 text-lg truncate">{{ getDesign.attribute || '温度' }}</div>
  66 + </div>
  67 + </main>
  68 +</template>
... ...
  1 +import { TextComponent1Config } from './TextComponent1';
  2 +import { TextComponent2Config } from './TextComponent2';
  3 +import { TextComponent3Config } from './TextComponent3';
  4 +import { TextComponent4Config } from './TextComponent4';
  5 +
  6 +export const TextList = [
  7 + TextComponent1Config,
  8 + TextComponent2Config,
  9 + TextComponent3Config,
  10 + TextComponent4Config,
  11 +];
... ...
  1 +export enum ComponentEnum {
  2 + TEXT_COMPONENT_1 = 'TEXT_COMPONENT_1',
  3 + TEXT_COMPONENT_2 = 'TEXT_COMPONENT_2',
  4 + TEXT_COMPONENT_3 = 'TEXT_COMPONENT_3',
  5 + TEXT_COMPONENT_4 = 'TEXT_COMPONENT_4',
  6 + TEXT_COMPONENT_5 = 'TEXT_COMPONENT_5',
  7 +}
  8 +
  9 +export enum ComponentNameEnum {
  10 + TEXT_COMPONENT_1 = '文本组件1',
  11 + TEXT_COMPONENT_2 = '文本组件2',
  12 + TEXT_COMPONENT_3 = '文本组件3',
  13 + TEXT_COMPONENT_4 = '文本组件4',
  14 + TEXT_COMPONENT_5 = '文本组件5',
  15 +}
... ...
  1 +import { unref } from 'vue';
  2 +import { useSelectWidgetKeys, useSelectWidgetMode } from '../../dataSourceBindPanel/useContext';
  3 +import { PackagesCategoryEnum } from '../index.type';
  4 +import { getDeviceProfile } from '/@/api/alarm/position';
  5 +import { getDeviceAttributes, getMeetTheConditionsDevice } from '/@/api/dataBoard';
  6 +import { DeviceAttributeParams, MasterDeviceList } from '/@/api/dataBoard/model';
  7 +import { DeviceTypeEnum } from '/@/api/device/model/deviceModel';
  8 +import { ModelOfMatterParams } from '/@/api/device/model/modelOfMatterModel';
  9 +import { getModelServices } from '/@/api/device/modelOfMatter';
  10 +import { findDictItemByCode } from '/@/api/system/dict';
  11 +import { FormSchema, useComponentRegister } from '/@/components/Form';
  12 +import { DataTypeEnum } from '/@/components/Form/src/externalCompns/components/StructForm/config';
  13 +import { OrgTreeSelect } from '/@/views/common/OrgTreeSelect';
  14 +import { TransportTypeEnum } from '/@/views/device/profiles/components/TransportDescript/const';
  15 +import { CommandTypeEnum } from '/@/views/rule/linkedge/config/config.data';
  16 +import { DataActionModeEnum } from '/@/enums/toolEnum';
  17 +
  18 +useComponentRegister('OrgTreeSelect', OrgTreeSelect);
  19 +
  20 +export interface CommonDataSourceBindValueType extends Record<DataSourceField, string> {
  21 + customCommand?: {
  22 + transportType?: string;
  23 + service?: string;
  24 + command?: string;
  25 + commandType?: string;
  26 + };
  27 +}
  28 +
  29 +export enum DataSourceField {
  30 + IS_GATEWAY_DEVICE = 'gatewayDevice',
  31 + DEVICE_TYPE = 'deviceType',
  32 + TRANSPORT_TYPE = 'transportType',
  33 + ORIGINATION_ID = 'organizationId',
  34 + DEVICE_ID = 'deviceId',
  35 + DEVICE_PROFILE_ID = 'deviceProfileId',
  36 + ATTRIBUTE = 'attribute',
  37 + ATTRIBUTE_RENAME = 'attributeRename',
  38 + DEVICE_NAME = 'deviceName',
  39 + DEVICE_RENAME = 'deviceRename',
  40 + LONGITUDE_ATTRIBUTE = 'longitudeAttribute',
  41 + LATITUDE_ATTRIBUTE = 'latitudeAttribute',
  42 +
  43 + COMMAND = 'command',
  44 + COMMAND_TYPE = 'commandType',
  45 + SERVICE = 'service',
  46 +}
  47 +
  48 +const isTcpProfile = (transportType: string) => transportType === TransportTypeEnum.TCP;
  49 +
  50 +const isControlComponent = (category?: string) => PackagesCategoryEnum.CONTROL === category;
  51 +
  52 +const getDeviceService = async (deviceProfileId: string) => {
  53 + try {
  54 + const data = await getModelServices({ deviceProfileId });
  55 + if (data)
  56 + return data.map((item) => ({ ...item, label: item.functionName, value: item.identifier }));
  57 + } catch (error) {}
  58 + return [];
  59 +};
  60 +
  61 +const getDeviceAttribute = async (params: DeviceAttributeParams) => {
  62 + try {
  63 + const data = await getDeviceAttributes(params);
  64 + if (data) return data.map((item) => ({ label: item.name, value: item.identifier }));
  65 + } catch (error) {}
  66 + return [];
  67 +};
  68 +
  69 +export const commonDataSourceSchemas = (): FormSchema[] => {
  70 + const mode = useSelectWidgetMode();
  71 + const isUpdate = unref(mode) === DataActionModeEnum.UPDATE;
  72 + const selectWidgetKeys = useSelectWidgetKeys();
  73 + const category = unref(selectWidgetKeys).categoryKey;
  74 +
  75 + return [
  76 + {
  77 + field: DataSourceField.IS_GATEWAY_DEVICE,
  78 + component: 'Switch',
  79 + label: '是否是网关设备',
  80 + show: false,
  81 + },
  82 + {
  83 + field: DataSourceField.DEVICE_NAME,
  84 + component: 'Input',
  85 + label: '设备名',
  86 + show: false,
  87 + },
  88 + {
  89 + field: DataSourceField.TRANSPORT_TYPE,
  90 + component: 'Input',
  91 + label: '设备配置类型',
  92 + show: false,
  93 + },
  94 + {
  95 + field: DataSourceField.DEVICE_TYPE,
  96 + component: 'ApiSelect',
  97 + label: '设备类型',
  98 + colProps: { span: 8 },
  99 + rules: [{ message: '请选择设备类型', required: true }],
  100 + componentProps: ({ formActionType }) => {
  101 + const { setFieldsValue } = formActionType;
  102 + return {
  103 + api: findDictItemByCode,
  104 + params: {
  105 + dictCode: 'device_type',
  106 + },
  107 + valueField: 'itemValue',
  108 + labelField: 'itemText',
  109 + placeholder: '请选择设备类型',
  110 + onChange: (value: DeviceTypeEnum) => {
  111 + setFieldsValue({
  112 + [DataSourceField.IS_GATEWAY_DEVICE]: value === DeviceTypeEnum.GATEWAY,
  113 + [DataSourceField.DEVICE_PROFILE_ID]: null,
  114 + [DataSourceField.DEVICE_ID]: null,
  115 + [DataSourceField.ATTRIBUTE]: null,
  116 + [DataSourceField.TRANSPORT_TYPE]: null,
  117 + });
  118 + },
  119 + getPopupContainer: () => document.body,
  120 + };
  121 + },
  122 + },
  123 + {
  124 + field: DataSourceField.DEVICE_PROFILE_ID,
  125 + component: 'ApiSelect',
  126 + label: '产品',
  127 + colProps: { span: 8 },
  128 + rules: [{ required: true, message: '产品为必填项' }],
  129 + componentProps: ({ formActionType, formModel }) => {
  130 + const { setFieldsValue } = formActionType;
  131 + const deviceType = formModel[DataSourceField.DEVICE_TYPE];
  132 + return {
  133 + api: async () => {
  134 + if (!deviceType) return [];
  135 + const list = await getDeviceProfile(deviceType);
  136 + if (isUpdate) {
  137 + const deviceProfileId = formModel[DataSourceField.DEVICE_PROFILE_ID];
  138 + const record = list.find((item) => item.id === deviceProfileId);
  139 + setFieldsValue({ [DataSourceField.TRANSPORT_TYPE]: record?.transportType });
  140 + }
  141 + return list;
  142 + },
  143 + labelField: 'name',
  144 + valueField: 'id',
  145 + placeholder: '请选择产品',
  146 + onChange: (_, option = {} as Record<'transportType', string>) => {
  147 + setFieldsValue({
  148 + [DataSourceField.DEVICE_ID]: null,
  149 + [DataSourceField.ATTRIBUTE]: null,
  150 + [DataSourceField.TRANSPORT_TYPE]: option[DataSourceField.TRANSPORT_TYPE],
  151 + });
  152 + },
  153 + getPopupContainer: () => document.body,
  154 + };
  155 + },
  156 + },
  157 + {
  158 + field: DataSourceField.ORIGINATION_ID,
  159 + component: 'OrgTreeSelect',
  160 + label: '组织',
  161 + colProps: { span: 8 },
  162 + rules: [{ required: true, message: '组织为必填项' }],
  163 + componentProps({ formActionType }) {
  164 + const { setFieldsValue } = formActionType;
  165 + return {
  166 + placeholder: '请选择组织',
  167 + onChange() {
  168 + setFieldsValue({
  169 + [DataSourceField.DEVICE_ID]: null,
  170 + });
  171 + },
  172 + showCreate: false,
  173 + getPopupContainer: () => document.body,
  174 + };
  175 + },
  176 + },
  177 + {
  178 + field: DataSourceField.DEVICE_PROFILE_ID,
  179 + component: 'Input',
  180 + label: '',
  181 + show: false,
  182 + },
  183 + {
  184 + field: DataSourceField.DEVICE_ID,
  185 + component: 'ApiSelect',
  186 + label: '设备',
  187 + colProps: { span: 8 },
  188 + rules: [{ required: true, message: '设备名称为必填项' }],
  189 + componentProps({ formModel, formActionType }) {
  190 + const { setFieldsValue } = formActionType;
  191 + const organizationId = formModel[DataSourceField.ORIGINATION_ID];
  192 + const deviceProfileId = formModel[DataSourceField.DEVICE_PROFILE_ID];
  193 + const deviceType = formModel[DataSourceField.DEVICE_TYPE];
  194 +
  195 + return {
  196 + api: async () => {
  197 + if (organizationId) {
  198 + try {
  199 + const data = await getMeetTheConditionsDevice({
  200 + organizationId,
  201 + deviceProfileId,
  202 + deviceType,
  203 + });
  204 + if (data)
  205 + return data.map((item) => ({
  206 + ...item,
  207 + label: item.alias || item.name,
  208 + value: item.tbDeviceId,
  209 + deviceType: item.deviceType,
  210 + }));
  211 + } catch (error) {}
  212 + }
  213 + return [];
  214 + },
  215 + onChange(_value, record: MasterDeviceList) {
  216 + setFieldsValue({
  217 + [DataSourceField.DEVICE_NAME]: record?.label,
  218 + });
  219 + },
  220 + placeholder: '请选择设备',
  221 + getPopupContainer: () => document.body,
  222 + };
  223 + },
  224 + },
  225 + {
  226 + field: DataSourceField.ATTRIBUTE,
  227 + component: 'ApiSelect',
  228 + label: '属性',
  229 + colProps: { span: 8 },
  230 + rules: [{ required: true, message: '请选择属性' }],
  231 + ifShow: ({ model }) =>
  232 + !(isTcpProfile(model[DataSourceField.TRANSPORT_TYPE]) && isControlComponent(category!)),
  233 + componentProps({ formModel }) {
  234 + const deviceProfileId = formModel[DataSourceField.DEVICE_PROFILE_ID];
  235 + return {
  236 + api: async () => {
  237 + try {
  238 + if (deviceProfileId) {
  239 + return await getDeviceAttribute({
  240 + deviceProfileId,
  241 + dataType: isControlComponent(category!) ? DataTypeEnum.IS_BOOL : undefined,
  242 + });
  243 + }
  244 + } catch (error) {}
  245 + return [];
  246 + },
  247 + placeholder: '请选择属性',
  248 + getPopupContainer: () => document.body,
  249 + };
  250 + },
  251 + },
  252 + {
  253 + field: DataSourceField.COMMAND_TYPE,
  254 + component: 'ApiSelect',
  255 + label: '命令类型',
  256 + defaultValue: CommandTypeEnum.CUSTOM.toString(),
  257 + rules: [{ required: true, message: '请选择命令类型' }],
  258 + colProps: { span: 8 },
  259 + ifShow: ({ model }) =>
  260 + isControlComponent(category!) && isTcpProfile(model[DataSourceField.TRANSPORT_TYPE]),
  261 + componentProps: ({ formActionType }) => {
  262 + const { setFieldsValue } = formActionType;
  263 + return {
  264 + api: findDictItemByCode,
  265 + params: {
  266 + dictCode: 'custom_define',
  267 + },
  268 + labelField: 'itemText',
  269 + valueField: 'itemValue',
  270 + placeholder: '请选择命令类型',
  271 + onChange() {
  272 + setFieldsValue({ [DataSourceField.COMMAND]: null, [DataSourceField.SERVICE]: null });
  273 + },
  274 + };
  275 + },
  276 + },
  277 + {
  278 + field: DataSourceField.SERVICE,
  279 + component: 'ApiSelect',
  280 + label: '服务',
  281 + colProps: { span: 8 },
  282 + rules: [{ required: true, message: '请选择服务' }],
  283 + ifShow: ({ model }) =>
  284 + isControlComponent(category!) &&
  285 + model[DataSourceField.COMMAND_TYPE] === CommandTypeEnum.SERVICE.toString() &&
  286 + isTcpProfile(model[DataSourceField.TRANSPORT_TYPE]),
  287 + componentProps({ formModel, formActionType }) {
  288 + const { setFieldsValue } = formActionType;
  289 + const deviceProfileId = formModel[DataSourceField.DEVICE_PROFILE_ID];
  290 + const transportType = formModel[DataSourceField.TRANSPORT_TYPE];
  291 + if (isUpdate && ![deviceProfileId, transportType].every(Boolean))
  292 + return { placeholder: '请选择服务', getPopupContainer: () => document.body };
  293 + return {
  294 + api: async () => {
  295 + try {
  296 + if (deviceProfileId) {
  297 + return await getDeviceService(deviceProfileId);
  298 + }
  299 + } catch (error) {}
  300 + return [];
  301 + },
  302 + placeholder: '请选择服务',
  303 + getPopupContainer: () => document.body,
  304 + onChange(value: string, options: ModelOfMatterParams) {
  305 + const command = value ? (options.functionJson.inputData || [])[0].serviceCommand : null;
  306 + setFieldsValue({ [DataSourceField.COMMAND]: command });
  307 + },
  308 + };
  309 + },
  310 + },
  311 + {
  312 + field: DataSourceField.COMMAND,
  313 + component: 'Input',
  314 + label: '命令',
  315 + colProps: { span: 8 },
  316 + rules: [{ required: true, message: '请输入下发命令' }],
  317 + // 是控制组件 && 自定义命令 && 传输协议为TCP
  318 + ifShow: ({ model }) =>
  319 + isControlComponent(category!) &&
  320 + model[DataSourceField.COMMAND_TYPE] === CommandTypeEnum.CUSTOM.toString() &&
  321 + model[DataSourceField.TRANSPORT_TYPE] &&
  322 + isTcpProfile(model[DataSourceField.TRANSPORT_TYPE]),
  323 + componentProps: {
  324 + placeholder: '请输入下发命令',
  325 + },
  326 + },
  327 + {
  328 + field: DataSourceField.DEVICE_RENAME,
  329 + component: 'Input',
  330 + label: '设备名',
  331 + colProps: { span: 8 },
  332 + componentProps: {
  333 + placeholder: '设备重命名',
  334 + },
  335 + },
  336 + {
  337 + field: DataSourceField.ATTRIBUTE_RENAME,
  338 + component: 'Input',
  339 + label: '属性',
  340 + colProps: { span: 8 },
  341 + componentProps: {
  342 + placeholder: '属性重命名',
  343 + },
  344 + },
  345 + ];
  346 +};
... ...
  1 +export enum ComponentConfigFieldEnum {
  2 + FONT_COLOR = 'fontColor',
  3 + UNIT = 'unit',
  4 + ICON_COLOR = 'iconColor',
  5 + ICON = 'icon',
  6 + FIRST_PHASE_COLOR = 'firstPhaseColor',
  7 + SECOND_PHASE_COLOR = 'secondPhaseColor',
  8 + THIRD_PHASE_COLOR = 'thirdPhaseColor',
  9 + FIRST_PHASE_VALUE = 'firstPhaseValue',
  10 + SECOND_PHASE_VALUE = 'secondPhaseValue',
  11 + THIRD_PHASE_VALUE = 'thirdPhaseValue',
  12 + SHOW_DEVICE_NAME = 'showDeviceName',
  13 + GRADIENT_INFO = 'gradientInfo',
  14 +
  15 + FLOWMETER_CONFIG = 'flowmeterConfig',
  16 + WAVE_FIRST = 'waveFirst',
  17 + WAVE_SECOND = 'waveSecond',
  18 + WAVE_THIRD = 'waveThird',
  19 + BACKGROUND_COLOR = 'backgroundColor',
  20 +}
... ...
  1 +import { onMounted, onUnmounted, ref } from 'vue';
  2 +import { useInjectScript } from '/@/hooks/web/useInjectScript';
  3 +import { BAI_DU_MAP_GL_LIB, BAI_DU_MAP_TRACK_ANIMATION } from '/@/utils/fnUtils';
  4 +
  5 +export enum LoadStatusEnum {
  6 + LOADING = 'LOADING',
  7 + SUCCESS = 'SUCCESS',
  8 + ERROR = 'ERROR',
  9 +}
  10 +
  11 +export const useBaiduMapSDK = (completeCallback: Fn) => {
  12 + const loadGLKey = 'loadBaiMapGL';
  13 + const loadGLLibKey = 'loadBaiMapGLLib';
  14 + const BaiduMapGLGlobalName = 'BMapGL';
  15 + const BaiduMapGLLibGlobalName = 'BMapGLLib';
  16 + const hasBMapGLFlag = Reflect.has(window, BaiduMapGLGlobalName);
  17 + const hasBMapGLLibFlag = Reflect.has(window, BaiduMapGLLibGlobalName);
  18 + const BMapGLRepeatLoadFlag = hasBMapGLFlag || Reflect.has(window, loadGLKey);
  19 + const BMapGLLibRepeatLoadFlag = hasBMapGLLibFlag || Reflect.has(window, loadGLLibKey);
  20 +
  21 + if (!BMapGLRepeatLoadFlag) {
  22 + Reflect.set(window, loadGLKey, true);
  23 + const { toInject } = useInjectScript({ src: BAI_DU_MAP_GL_LIB });
  24 + toInject();
  25 + }
  26 +
  27 + if (!BMapGLLibRepeatLoadFlag) {
  28 + Reflect.set(window, loadGLLibKey, true);
  29 + const { toInject } = useInjectScript({ src: BAI_DU_MAP_TRACK_ANIMATION });
  30 + toInject();
  31 + }
  32 +
  33 + const waitFn = async () => {
  34 + return new Promise((resolve) => {
  35 + let interval: Nullable<NodeJS.Timer> = setInterval(() => {
  36 + const hasBMapGLFlag = Reflect.has(window, BaiduMapGLGlobalName);
  37 + const hasBMapGLLibFlag = Reflect.has(window, BaiduMapGLLibGlobalName);
  38 + if (hasBMapGLFlag && hasBMapGLLibFlag) {
  39 + resolve('success');
  40 + status.value = LoadStatusEnum.SUCCESS;
  41 + loading.value = false;
  42 + clearInterval(interval!);
  43 + interval = null;
  44 + clearTimeout(timeout!);
  45 + timeout = null;
  46 + }
  47 + }, 300);
  48 +
  49 + let timeout: Nullable<NodeJS.Timeout> = setTimeout(() => {
  50 + status.value = LoadStatusEnum.ERROR;
  51 + clearTimeout(timeout!);
  52 + timeout = null;
  53 + resolve('error');
  54 + }, 10000);
  55 + });
  56 + };
  57 +
  58 + const loading = ref(true);
  59 +
  60 + const status = ref(LoadStatusEnum.LOADING);
  61 +
  62 + onMounted(async () => {
  63 + await waitFn();
  64 + completeCallback?.();
  65 + loading.value = false;
  66 + });
  67 +
  68 + onUnmounted(() => {
  69 + Reflect.deleteProperty(window, loadGLKey);
  70 + Reflect.deleteProperty(window, loadGLLibKey);
  71 + });
  72 +
  73 + return {
  74 + loading,
  75 + status,
  76 + };
  77 +};
... ...
  1 +export const useComponentKeys = (scope: string) => {
  2 + const key = scope;
  3 +
  4 + const conKey = `V${scope}`;
  5 +
  6 + const configConKey = `VC${scope}`;
  7 +
  8 + const datasourceConKey = `VD${scope}`;
  9 +
  10 + return { key, conKey, configConKey, datasourceConKey };
  11 +};
... ...
  1 +import { CSSProperties, computed, unref, watch } from 'vue';
  2 +import { ComponentPropsConfigType } from '../index.type';
  3 +import { componentOptionsInitConfig } from '../publicConfig';
  4 +
  5 +export const useComponentScale = (props: { config: ComponentPropsConfigType }, onScale?: Fn) => {
  6 + const getRatio = computed(() => {
  7 + try {
  8 + const { option, attr } = props.config;
  9 + const { w, h } = attr;
  10 + const { widthPx, heightPx, itemWidthRatio, itemHeightRatio } = option;
  11 +
  12 + const currentW = (widthPx * itemWidthRatio) / 100;
  13 + const currentH = (heightPx * itemHeightRatio) / 100;
  14 +
  15 + const widthScaleRatio = currentW / w;
  16 + const heightScaleRatio = currentH / h;
  17 +
  18 + return Math.min(widthScaleRatio, heightScaleRatio);
  19 + } catch (error) {
  20 + return 1;
  21 + }
  22 + });
  23 +
  24 + const getScaleRadio = computed(() => {
  25 + try {
  26 + const { persetOption = {} } = props.config;
  27 + const {
  28 + maxScale = componentOptionsInitConfig.maxScale,
  29 + minScale = componentOptionsInitConfig.minScale,
  30 + } = persetOption;
  31 +
  32 + let ratio = unref(getRatio);
  33 + ratio = ratio > maxScale! ? maxScale! : ratio < minScale! ? minScale! : ratio;
  34 +
  35 + return ratio;
  36 + } catch (error) {
  37 + return 1;
  38 + }
  39 + });
  40 +
  41 + const getScale = computed<CSSProperties>(() => {
  42 + return { transform: `scale(${unref(getScaleRadio)})` };
  43 + });
  44 +
  45 + onScale &&
  46 + watch(getRatio, () => {
  47 + onScale?.();
  48 + });
  49 +
  50 + return { getScale, getScaleRadio, getRatio };
  51 +};
... ...
  1 +import { createComponent } from '..';
  2 +import { transformComponentKey } from '../componentMap';
  3 +import { ConfigType, CreateComponentType } from '../index.type';
  4 +
  5 +export const useGetComponentConfig = (
  6 + key: string,
  7 + options: Recordable = {}
  8 +): CreateComponentType => {
  9 + try {
  10 + const config = createComponent({ key: transformComponentKey(key) } as ConfigType, options);
  11 + return config;
  12 + } catch (error) {
  13 + console.error(`can not get component config by component key (${key})`);
  14 + return {} as CreateComponentType;
  15 + }
  16 +};
... ...
... ... @@ -12,14 +12,16 @@ export function useSendCommand() {
12 12 createMessage.error('下发指令失败');
13 13 return false;
14 14 };
  15 +
15 16 const sendCommand = async (record: DataSource, value: any) => {
16   - if (!record) return error();
17   - const { customCommand, attribute } = record;
  17 + if (!record) return false;
  18 + const { customCommand, attribute } = record || {};
18 19
19 20 const { deviceId } = record;
20   - if (!deviceId) return error();
21   - loading.value = true;
  21 + if (!deviceId) return false;
  22 +
22 23 try {
  24 + loading.value = true;
23 25 let params: string | Recordable = {
24 26 [attribute!]: Number(value),
25 27 };
... ... @@ -50,7 +52,7 @@ export function useSendCommand() {
50 52 }
51 53 };
52 54 return {
53   - sendCommand,
54 55 loading,
  56 + sendCommand,
55 57 };
56 58 }
... ...
  1 +import { Ref, computed, onUnmounted, unref, watch } from 'vue';
  2 +import { WidgetDataType } from '../../palette/hooks/useDataSource';
  3 +import { useWebSocket } from '@vueuse/core';
  4 +import { useGlobSetting } from '/@/hooks/setting';
  5 +import { isShareMode } from '/@/views/sys/share/hook';
  6 +import { getJwtToken, getShareJwtToken } from '/@/utils/auth';
  7 +import { isNullAndUnDef } from '/@/utils/is';
  8 +import {
  9 + ComponentPropsConfigType,
  10 + DataFetchUpdateFn,
  11 + EntityTypeEnum,
  12 + MultipleDataFetchUpdateFn,
  13 + ReceiveMessageType,
  14 + ScopeTypeEnum,
  15 + SubscribeMessageItemType,
  16 + SubscribeMessageType,
  17 +} from '../index.type';
  18 +import { DataSource } from '../../palette/types';
  19 +
  20 +interface DeviceGroupMapType {
  21 + subscriptionId: number;
  22 + attributes: Set<string>;
  23 + subscriptionGroup: Record<'uuid' | 'attribute', string>[];
  24 +}
  25 +
  26 +interface ComponentUpdateFnMapValueType {
  27 + fn: MultipleDataFetchUpdateFn;
  28 + attributes: string[];
  29 +}
  30 +
  31 +const parseMessage = (text: string): ReceiveMessageType => {
  32 + try {
  33 + return JSON.parse(text);
  34 + } catch (error) {
  35 + return {} as ReceiveMessageType;
  36 + }
  37 +};
  38 +
  39 +class Subscriber {
  40 + subscribeId = 0;
  41 +
  42 + deviceGroupMap = new Map<string, DeviceGroupMapType>();
  43 +
  44 + subscriptionMap = new Map<number, string>();
  45 +
  46 + componentUpdateFnMap = new Map<string, DataFetchUpdateFn>();
  47 +
  48 + componentGroupUpdateFnMap = new Map<string, ComponentUpdateFnMapValueType[]>();
  49 +
  50 + getNextSubscribeId() {
  51 + return this.subscribeId++;
  52 + }
  53 +
  54 + clearSubscriber = () => {
  55 + this.deviceGroupMap.clear();
  56 + this.subscriptionMap.clear();
  57 + this.componentUpdateFnMap.clear();
  58 + };
  59 +
  60 + addSubscriber = (info: Record<'deviceId' | 'slaveDeviceId' | 'attribute' | 'uuid', string>) => {
  61 + const { deviceId, attribute, uuid } = info;
  62 + if (!this.deviceGroupMap.has(deviceId)) {
  63 + this.deviceGroupMap.set(deviceId, {
  64 + subscriptionId: this.getNextSubscribeId(),
  65 + attributes: new Set(),
  66 + subscriptionGroup: [],
  67 + });
  68 + }
  69 +
  70 + const groupInfo = this.deviceGroupMap.get(deviceId);
  71 + groupInfo?.attributes.add(attribute);
  72 + groupInfo?.subscriptionGroup.push({ uuid, attribute });
  73 +
  74 + this.subscriptionMap.set(groupInfo!.subscriptionId, deviceId);
  75 + };
  76 +
  77 + genBasicMessage(unsubscribe = false) {
  78 + const message = Array.from(this.deviceGroupMap.entries()).map(([deviceId, value]) => {
  79 + const { subscriptionId, attributes } = value;
  80 + return {
  81 + cmdId: subscriptionId,
  82 + entityId: deviceId,
  83 + keys: Array.from(attributes.values()).join(','),
  84 + entityType: EntityTypeEnum.DEVICE,
  85 + scope: ScopeTypeEnum.LATEST_TELEMERY,
  86 + ...(unsubscribe ? { unsubscribe } : {}),
  87 + } as SubscribeMessageItemType;
  88 + });
  89 + return { tsSubCmds: message } as SubscribeMessageType;
  90 + }
  91 +
  92 + genUnSubscribeMessage() {
  93 + return this.genBasicMessage(true);
  94 + }
  95 +
  96 + genSubscribeMessage() {
  97 + return this.genBasicMessage();
  98 + }
  99 +
  100 + getScopeMessage(message: ReceiveMessageType, attribute: string[]) {
  101 + const data = attribute.reduce((prev, next) => {
  102 + return { ...prev, [next]: (message.data || {})[next] || [[]] };
  103 + }, {} as ReceiveMessageType['data']);
  104 +
  105 + const latestValues = attribute.reduce((prev, next) => {
  106 + return { ...prev, [next]: (message.latestValues || {})[next] || [[]] };
  107 + }, {} as ReceiveMessageType['data']);
  108 +
  109 + return {
  110 + subscriptionId: message.subscriptionId,
  111 + errorCode: message.errorCode,
  112 + errorMsg: message.errorMsg,
  113 + data,
  114 + latestValues,
  115 + } as ReceiveMessageType;
  116 + }
  117 +
  118 + trackUpdate(uuid: string, fn: Fn) {
  119 + if (!uuid || !fn) return;
  120 + this.componentUpdateFnMap.set(uuid, fn);
  121 + }
  122 +
  123 + trackUpdateGroup(deviceId: string, data: ComponentUpdateFnMapValueType) {
  124 + if (!deviceId || !data) return;
  125 + if (!this.componentGroupUpdateFnMap.has(deviceId))
  126 + this.componentGroupUpdateFnMap.set(deviceId, []);
  127 + const temp = this.componentGroupUpdateFnMap.get(deviceId);
  128 + temp?.push(data);
  129 + }
  130 +
  131 + triggerUpdate(message: ReceiveMessageType) {
  132 + const { subscriptionId } = message;
  133 + if (isNullAndUnDef(subscriptionId)) return;
  134 + const deviceId = this.subscriptionMap.get(subscriptionId);
  135 + if (!deviceId) return;
  136 + const deviceGroup = this.deviceGroupMap.get(deviceId);
  137 + if (!deviceGroup) return;
  138 + const { subscriptionGroup } = deviceGroup;
  139 +
  140 + const updateGroups = this.componentGroupUpdateFnMap.get(deviceId);
  141 +
  142 + if (updateGroups) {
  143 + (updateGroups || []).forEach((item) => {
  144 + const { attributes, fn } = item;
  145 + try {
  146 + if (!fn) return;
  147 + fn?.(this.getScopeMessage(message, attributes), attributes);
  148 + } catch (error) {
  149 + console.error(`deviceId: ${deviceId}`);
  150 + throw error;
  151 + }
  152 + });
  153 + }
  154 +
  155 + subscriptionGroup.forEach((item) => {
  156 + const { attribute, uuid } = item;
  157 + const updateFn = this.componentUpdateFnMap.get(uuid);
  158 + try {
  159 + if (!updateFn) return;
  160 + updateFn?.(this.getScopeMessage(message, [attribute]), attribute);
  161 + } catch (error) {
  162 + console.error(`uuid: ${uuid}`);
  163 + throw error;
  164 + }
  165 + });
  166 + }
  167 +}
  168 +
  169 +const subscriber = new Subscriber();
  170 +
  171 +export const useSocket = (dataSourceRef: Ref<WidgetDataType[]>) => {
  172 + let initied = false;
  173 +
  174 + const { socketUrl } = useGlobSetting();
  175 +
  176 + const token = isShareMode() ? getShareJwtToken() : getJwtToken();
  177 +
  178 + const server = `${socketUrl}${token}`;
  179 +
  180 + const { send, data, close } = useWebSocket(server, {
  181 + onMessage() {
  182 + initied = true;
  183 + try {
  184 + const message = parseMessage(unref(data));
  185 + subscriber.triggerUpdate(message);
  186 + } catch (error) {
  187 + throw Error(error as string);
  188 + }
  189 + },
  190 + });
  191 +
  192 + const initSubscribe = () => {
  193 + subscriber.clearSubscriber();
  194 + unref(dataSourceRef).forEach((item) => {
  195 + item.dataSource.forEach((temp) => {
  196 + const { deviceId, slaveDeviceId, attribute, uuid } = temp;
  197 + subscriber.addSubscriber({ deviceId, slaveDeviceId, attribute, uuid });
  198 + });
  199 + });
  200 + };
  201 +
  202 + watch(
  203 + () => dataSourceRef.value,
  204 + (value) => {
  205 + if (value.length) {
  206 + if (initied) {
  207 + const message = JSON.stringify(subscriber.genUnSubscribeMessage());
  208 + send(message);
  209 + }
  210 +
  211 + initSubscribe();
  212 +
  213 + const message = JSON.stringify(subscriber.genSubscribeMessage());
  214 + send(message);
  215 + }
  216 + }
  217 + );
  218 +
  219 + onUnmounted(() => {
  220 + close();
  221 + });
  222 +};
  223 +
  224 +export const useDataFetch = (
  225 + props: { config: ComponentPropsConfigType },
  226 + updateFn: DataFetchUpdateFn
  227 +) => {
  228 + const getBindAttribute = computed(() => {
  229 + const { config } = props;
  230 + const { option } = config as ComponentPropsConfigType<Recordable, DataSource>;
  231 + return option.attribute;
  232 + });
  233 +
  234 + if (!unref(getBindAttribute)) return;
  235 +
  236 + const getUUID = computed(() => {
  237 + return props.config.option.uuid;
  238 + });
  239 +
  240 + watch(
  241 + () => getUUID,
  242 + () => {
  243 + subscriber.trackUpdate(unref(getUUID), updateFn);
  244 + },
  245 + {
  246 + immediate: true,
  247 + }
  248 + );
  249 + return { getUUID, getBindAttribute };
  250 +};
  251 +
  252 +export const useMultipleDataFetch = (
  253 + props: { config: ComponentPropsConfigType },
  254 + updateFn: MultipleDataFetchUpdateFn
  255 +) => {
  256 + const getBindAttributes = computed(() => {
  257 + const attributes = props.config.option.dataSource?.map((item) => item.attribute);
  258 + return [...new Set(attributes)];
  259 + });
  260 +
  261 + if (unref(getBindAttributes).length) return;
  262 +
  263 + const getDeviceId = computed(() => {
  264 + return props.config.option.dataSource?.at(0)?.deviceId;
  265 + });
  266 +
  267 + watch(
  268 + () => getDeviceId,
  269 + () => {
  270 + subscriber.trackUpdateGroup(unref(getDeviceId)!, {
  271 + attributes: unref(getBindAttributes),
  272 + fn: updateFn,
  273 + });
  274 + },
  275 + {
  276 + immediate: true,
  277 + }
  278 + );
  279 +
  280 + return { getDeviceId, getBindAttributes };
  281 +};
... ...
  1 +import { ConfigType, FetchComFlagTypeEnum, PublicComponentOptions } from './index.type';
  2 +
  3 +const configModules = import.meta.globEager('./components/**/config.vue');
  4 +const viewModules = import.meta.globEager('./components/**/index.vue');
  5 +const datasourceModules = import.meta.globEager('./components/**/datasource.vue');
  6 +const createInstaceModules = import.meta.globEager('./components/**/config.ts');
  7 +
  8 +const findModule = (componentName: string, module: Recordable) => {
  9 + for (const key in module) {
  10 + const urlSplit = key.split('/');
  11 + if (urlSplit[urlSplit.length - 2] === componentName) {
  12 + return module[key] as Record<'default', any>;
  13 + }
  14 + }
  15 +};
  16 +
  17 +/**
  18 + * @description 获取组件
  19 + * @param {string} componentName 组件名称
  20 + * @param {FetchComFlagTypeEnum} flag 标识 0展示组件 1配置组件 2数据源组件
  21 + */
  22 +export const fetchComponent = (componentName: string, flag: FetchComFlagTypeEnum) => {
  23 + const module =
  24 + flag === FetchComFlagTypeEnum.VIEW
  25 + ? viewModules
  26 + : flag === FetchComFlagTypeEnum.CONFIG
  27 + ? configModules
  28 + : datasourceModules;
  29 + return findModule(componentName, module);
  30 +};
  31 +
  32 +export const fetchViewComponent = (configType: ConfigType) => {
  33 + const { key } = configType;
  34 + return fetchComponent(key, FetchComFlagTypeEnum.VIEW)?.default;
  35 +};
  36 +
  37 +export const fetchConfigComponent = (configType: ConfigType) => {
  38 + const { key } = configType;
  39 + return fetchComponent(key, FetchComFlagTypeEnum.CONFIG)?.default;
  40 +};
  41 +
  42 +export const fetchDatasourceComponent = (configType: ConfigType) => {
  43 + const { key } = configType;
  44 + return fetchComponent(key, FetchComFlagTypeEnum.DATASOURCE_FORM)?.default;
  45 +};
  46 +
  47 +/**
  48 + * @description 获取目标组件配置信息
  49 + * @param targetData
  50 + */
  51 +export const createComponent = (
  52 + targetData: ConfigType,
  53 + options?: Partial<PublicComponentOptions>
  54 +) => {
  55 + const { key } = targetData;
  56 + const module = findModule(key, createInstaceModules);
  57 + const instance = module?.default;
  58 + if (instance) {
  59 + return new instance(options || {});
  60 + }
  61 +};
... ...
  1 +import { ComponentInfoGradientInfoType, DataSource, FlowmeterConfigType } from '../palette/types';
  2 +import { ComponentConfigFieldEnum } from './enum';
  3 +
  4 +/**
  5 + * @description 获取组件
  6 + */
  7 +export enum FetchComFlagTypeEnum {
  8 + VIEW,
  9 + CONFIG,
  10 + DATASOURCE_FORM,
  11 +}
  12 +
  13 +/**
  14 + * @description 包分类名称
  15 + */
  16 +export enum PackagesCategoryNameEnum {
  17 + TEXT = '文本组件',
  18 + INSTRUMENT = '仪表组件',
  19 + PICTURE = '图片组件',
  20 + CONTROL = '控制组件',
  21 + MAP = '地图组件',
  22 + FLOWMETER = '流量计',
  23 + OTHER = '其他',
  24 +}
  25 +
  26 +/**
  27 + * @description 包分类枚举
  28 + */
  29 +export enum PackagesCategoryEnum {
  30 + TEXT = 'TEXT',
  31 + INSTRUMENT = 'INSTRUMENT',
  32 + PICTURE = 'PICTURE',
  33 + CONTROL = 'CONTROL',
  34 + MAP = 'MAP',
  35 + FLOWMETER = 'FLOWMETER',
  36 + OTHER = 'FLOWMETER',
  37 +}
  38 +
  39 +/**
  40 + * @description 组件配置
  41 + */
  42 +export interface ConfigType {
  43 + /**
  44 + * @description
  45 + */
  46 + key: string;
  47 +
  48 + /**
  49 + * @description 组件key
  50 + */
  51 + conKey: string;
  52 +
  53 + /**
  54 + * @description 组件数据源组件key
  55 + */
  56 + datasourceConKey: string;
  57 +
  58 + /**
  59 + * @description 组件配置组件key
  60 + */
  61 + configConKey: string;
  62 + title: string;
  63 + category?: string;
  64 + categoryName?: string;
  65 + package: string;
  66 +}
  67 +
  68 +/**
  69 + * @description 创建组件类型
  70 + */
  71 +export interface CreateComponentType<P = PublicPresetOptions, O = Partial<DataSource>> {
  72 + key: string;
  73 + componentConfig: ConfigType;
  74 + option: PublicComponentOptions & O;
  75 + persetOption?: P;
  76 +}
  77 +
  78 +/**
  79 + * @description 组件公共配置
  80 + */
  81 +export interface PublicConfigType {
  82 + id: string;
  83 + attr: { x: number; y: number; w: number; h: number };
  84 +}
  85 +
  86 +export interface PublicPresetOptions {
  87 + maxScale?: number;
  88 + minScale?: number;
  89 + componetDesign?: boolean;
  90 + multipleDataSourceComponent?: boolean;
  91 + [ComponentConfigFieldEnum.FONT_COLOR]?: string;
  92 + [ComponentConfigFieldEnum.UNIT]?: string;
  93 + [ComponentConfigFieldEnum.ICON_COLOR]?: string;
  94 + [ComponentConfigFieldEnum.ICON]?: string;
  95 + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]?: boolean;
  96 + [ComponentConfigFieldEnum.GRADIENT_INFO]?: ComponentInfoGradientInfoType[];
  97 + [ComponentConfigFieldEnum.FLOWMETER_CONFIG]?: FlowmeterConfigType;
  98 + [key: string]: any;
  99 +}
  100 +
  101 +export interface PublicComponentOptions {
  102 + uuid: string;
  103 + widthPx: number;
  104 + heightPx: number;
  105 + itemWidthRatio: number;
  106 + itemHeightRatio: number;
  107 +
  108 + dataSource?: DataSource[];
  109 + [key: string]: any;
  110 +}
  111 +
  112 +export interface ComponentPropsConfigType<P = PublicPresetOptions, O = DataSource>
  113 + extends CreateComponentType<P, O>,
  114 + PublicConfigType {}
  115 +
  116 +export interface PackagesType {
  117 + [PackagesCategoryEnum.TEXT]: ConfigType[];
  118 + [PackagesCategoryEnum.PICTURE]: ConfigType[];
  119 + [PackagesCategoryEnum.INSTRUMENT]: ConfigType[];
  120 + [PackagesCategoryEnum.CONTROL]: ConfigType[];
  121 + [PackagesCategoryEnum.MAP]: ConfigType[];
  122 + [PackagesCategoryEnum.FLOWMETER]: ConfigType[];
  123 + [PackagesCategoryEnum.OTHER]: ConfigType[];
  124 +}
  125 +
  126 +export interface SubscribeMessageType {
  127 + tsSubCmds: SubscribeMessageItemType[];
  128 +}
  129 +
  130 +export interface SubscribeMessageItemType {
  131 + cmdId: number;
  132 + entityId: string;
  133 + entityType: string;
  134 + keys?: string;
  135 + scope?: string;
  136 + unsubscribe?: boolean;
  137 +}
  138 +
  139 +export interface ReceiveMessageType {
  140 + subscriptionId: number;
  141 + errorCode: number;
  142 + errorMsg: Nullable<string>;
  143 + data: Record<string, [number, string][]>;
  144 + latestValues: Record<string, number>;
  145 +}
  146 +
  147 +export enum EntityTypeEnum {
  148 + DEVICE = 'DEVICE',
  149 +}
  150 +
  151 +export enum ScopeTypeEnum {
  152 + LATEST_TELEMERY = 'LATEST_TELEMERY',
  153 +}
  154 +
  155 +export type DataFetchUpdateFn = (message: ReceiveMessageType, attr: string) => void;
  156 +
  157 +export type MultipleDataFetchUpdateFn = (message: ReceiveMessageType, attr: string[]) => void;
  158 +
  159 +// 旧 组件key
  160 +// TEXT_COMPONENT_1 = 'text-component-1',
  161 +// TEXT_COMPONENT_2 = 'text-component-2',
  162 +// TEXT_COMPONENT_3 = 'text-component-3',
  163 +// TEXT_COMPONENT_4 = 'text-component-4',
  164 +// TEXT_COMPONENT_5 = 'text-component-5',
  165 +// INSTRUMENT_COMPONENT_1 = 'instrument-component-1',
  166 +// INSTRUMENT_COMPONENT_2 = 'instrument-component-2',
  167 +// DIGITAL_DASHBOARD_COMPONENT = 'digital-dashboard-component',
  168 +// PICTURE_COMPONENT_1 = 'picture-component-1',
  169 +// CONTROL_COMPONENT_TOGGLE_SWITCH = 'control-component-toggle-switch',
  170 +// CONTROL_COMPONENT_SWITCH_WITH_ICON = 'control-component-switch-with-icon',
  171 +// CONTROL_COMPONENT_SLIDING_SWITCH = 'control-component-sliding-switch',
  172 +// MAP_COMPONENT_TRACK_REAL = 'map-component-track-real',
  173 +// MAP_COMPONENT_TRACK_HISTORY = 'map-component-track-history',
... ...
  1 +import { ControlList } from './components/Control';
  2 +import { FlowmeterList } from './components/Flowmeter';
  3 +import { InstrumentList } from './components/Instrument';
  4 +import { MapList } from './components/Map';
  5 +import { PictureList } from './components/Picture';
  6 +import { TextList } from './components/Text';
  7 +import { PackagesCategoryEnum, PackagesType } from './index.type';
  8 +
  9 +export const packageList: PackagesType = {
  10 + [PackagesCategoryEnum.TEXT]: TextList,
  11 + [PackagesCategoryEnum.INSTRUMENT]: InstrumentList,
  12 + [PackagesCategoryEnum.PICTURE]: PictureList,
  13 + [PackagesCategoryEnum.CONTROL]: ControlList,
  14 + [PackagesCategoryEnum.MAP]: MapList,
  15 + [PackagesCategoryEnum.FLOWMETER]: FlowmeterList,
  16 +};
... ...
  1 +import { PublicConfigType, PublicPresetOptions } from './index.type';
  2 +import { buildUUID } from '/@/utils/uuid';
  3 +
  4 +export const componentInitConfig = { x: 50, y: 50, w: 240, h: 200 };
  5 +
  6 +export const componentOptionsInitConfig: PublicPresetOptions = { maxScale: 1, minScale: 0 };
  7 +
  8 +export class PublicConfigClass implements PublicConfigType {
  9 + public id = buildUUID();
  10 + public attr: Record<'x' | 'y' | 'w' | 'h', number> = { ...componentInitConfig };
  11 +}
... ...
  1 +import cloneDeep from 'lodash-es/cloneDeep';
  2 +import { ComponentConfig } from '.';
  3 +import {
  4 + ConfigType,
  5 + CreateComponentType,
  6 + PublicComponentOptions,
  7 + PublicPresetOptions,
  8 +} from '/@/views/visual/packages/index.type';
  9 +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig';
  10 +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  11 +
  12 +export const option: PublicPresetOptions = {
  13 + [ComponentConfigFieldEnum.FONT_COLOR]: '#',
  14 + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false,
  15 +};
  16 +
  17 +export default class Config extends PublicConfigClass implements CreateComponentType {
  18 + public key: string = ComponentConfig.key;
  19 +
  20 + public attr = { ...componentInitConfig };
  21 +
  22 + public componentConfig: ConfigType = cloneDeep(ComponentConfig);
  23 +
  24 + public persetOption = cloneDeep(option);
  25 +
  26 + public option: PublicComponentOptions;
  27 +
  28 + constructor(option: PublicComponentOptions) {
  29 + super();
  30 + this.option = { ...option };
  31 + }
  32 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum';
  3 + import { useForm, BasicForm } from '/@/components/Form';
  4 + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type';
  5 +
  6 + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({
  7 + schemas: [
  8 + {
  9 + field: ComponentConfigFieldEnum.FONT_COLOR,
  10 + label: '数值字体颜色',
  11 + component: 'ColorPicker',
  12 + changeEvent: 'update:value',
  13 + componentProps: {
  14 + defaultValue: '#FD7347',
  15 + },
  16 + },
  17 + {
  18 + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME,
  19 + label: '显示设备名称',
  20 + component: 'Checkbox',
  21 + },
  22 + ],
  23 + showActionButtonGroup: false,
  24 + labelWidth: 120,
  25 + baseColProps: {
  26 + span: 12,
  27 + },
  28 + });
  29 +
  30 + const getFormValues = () => {
  31 + return getFieldsValue();
  32 + };
  33 +
  34 + const setFormValues = (data: Recordable) => {
  35 + return setFieldsValue(data);
  36 + };
  37 +
  38 + defineExpose({
  39 + getFormValues,
  40 + setFormValues,
  41 + resetFormValues: resetFields,
  42 + } as PublicFormInstaceType);
  43 +</script>
  44 +
  45 +<template>
  46 + <BasicForm @register="register" />
  47 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { CreateComponentType } from '/@/views/visual/packages/index.type';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import { dataSourceSchema } from '/@/views/visual/board/detail/config/basicConfiguration';
  5 + import {
  6 + PublicComponentValueType,
  7 + PublicFormInstaceType,
  8 + } from '/@/views/visual/dataSourceBindPanel/index.type';
  9 +
  10 + const props = defineProps<{
  11 + values: PublicComponentValueType;
  12 + componentConfig: CreateComponentType;
  13 + }>();
  14 +
  15 + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({
  16 + labelWidth: 0,
  17 + showActionButtonGroup: false,
  18 + layout: 'horizontal',
  19 + labelCol: { span: 0 },
  20 + schemas: dataSourceSchema(false, props.componentConfig.componentConfig.key),
  21 + });
  22 +
  23 + const getFormValues = () => {
  24 + return getFieldsValue();
  25 + };
  26 +
  27 + const setFormValues = (record: Recordable) => {
  28 + return setFieldsValue(record);
  29 + };
  30 +
  31 + defineExpose({
  32 + getFormValues,
  33 + setFormValues,
  34 + validate,
  35 + resetFormValues: resetFields,
  36 + } as PublicFormInstaceType);
  37 +</script>
  38 +
  39 +<template>
  40 + <BasicForm @register="register" />
  41 +</template>
... ...
  1 +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys';
  2 +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type';
  3 +
  4 +const componentKeys = useComponentKeys('componentKeys');
  5 +
  6 +export const ComponentConfig: ConfigType = {
  7 + ...componentKeys,
  8 + title: '组件名',
  9 + package: PackagesCategoryEnum.TEXT,
  10 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type';
  3 + import { option } from './config';
  4 + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket';
  5 +
  6 + const props = defineProps<{
  7 + config: ComponentPropsConfigType<typeof option>;
  8 + }>();
  9 +
  10 + const updateFn: DataFetchUpdateFn = (_message) => {};
  11 +
  12 + useDataFetch(props, updateFn);
  13 +</script>
  14 +
  15 +<template>
  16 + <main class="w-full h-full flex flex-col justify-center items-center"> </main>
  17 +</template>
... ...
  1 +import moment from 'moment';
  2 +import { Moment } from 'moment';
  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 + return [
  40 + {
  41 + field: SchemaFiled.DEVICE_ID,
  42 + label: '设备名称',
  43 + component: 'Select',
  44 + rules: [{ required: true, message: '设备名称为必选项', type: 'string' }],
  45 + componentProps({ formActionType }) {
  46 + const { setFieldsValue } = formActionType;
  47 + return {
  48 + placeholder: '请选择设备',
  49 + onChange() {
  50 + setFieldsValue({ [SchemaFiled.KEYS]: null });
  51 + },
  52 + };
  53 + },
  54 + },
  55 + {
  56 + field: SchemaFiled.WAY,
  57 + label: '查询方式',
  58 + component: 'RadioGroup',
  59 + defaultValue: QueryWay.LATEST,
  60 + componentProps({ formActionType }) {
  61 + const { setFieldsValue } = formActionType;
  62 + return {
  63 + options: [
  64 + { label: '最后', value: QueryWay.LATEST },
  65 + { label: '时间段', value: QueryWay.TIME_PERIOD },
  66 + ],
  67 + onChange(event: ChangeEvent) {
  68 + (event.target as HTMLInputElement).value === QueryWay.LATEST
  69 + ? setFieldsValue({
  70 + [SchemaFiled.DATE_RANGE]: [],
  71 + [SchemaFiled.START_TS]: null,
  72 + [SchemaFiled.END_TS]: null,
  73 + })
  74 + : setFieldsValue({ [SchemaFiled.START_TS]: null });
  75 + },
  76 + getPopupContainer: () => document.body,
  77 + };
  78 + },
  79 + },
  80 + {
  81 + field: SchemaFiled.START_TS,
  82 + label: '最后数据',
  83 + component: 'Select',
  84 + ifShow({ values }) {
  85 + return values[SchemaFiled.WAY] === QueryWay.LATEST;
  86 + },
  87 + componentProps({ formActionType }) {
  88 + const { setFieldsValue } = formActionType;
  89 + return {
  90 + options: intervalOption,
  91 + onChange() {
  92 + setFieldsValue({ [SchemaFiled.INTERVAL]: null });
  93 + },
  94 + getPopupContainer: () => document.body,
  95 + };
  96 + },
  97 + rules: [{ required: true, message: '最后数据为必选项', type: 'number' }],
  98 + },
  99 + {
  100 + field: SchemaFiled.DATE_RANGE,
  101 + label: '时间段',
  102 + component: 'RangePicker',
  103 + ifShow({ values }) {
  104 + return values[SchemaFiled.WAY] === QueryWay.TIME_PERIOD;
  105 + },
  106 + rules: [{ required: true, message: '时间段为必选项' }],
  107 + componentProps({ formActionType }) {
  108 + const { setFieldsValue } = formActionType;
  109 + let dates: Moment[] = [];
  110 + return {
  111 + showTime: {
  112 + defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
  113 + },
  114 + onCalendarChange(value: Moment[]) {
  115 + dates = value;
  116 + },
  117 + disabledDate(current: Moment) {
  118 + if (!dates || dates.length === 0 || !current) {
  119 + return false;
  120 + }
  121 + const diffDate = current.diff(dates[0], 'years', true);
  122 + return Math.abs(diffDate) > 1;
  123 + },
  124 + onChange() {
  125 + dates = [];
  126 + setFieldsValue({ [SchemaFiled.INTERVAL]: null });
  127 + },
  128 + getPopupContainer: () => document.body,
  129 + };
  130 + },
  131 + colProps: useGridLayout(2, 2, 2, 2, 2, 2) as unknown as ColEx,
  132 + },
  133 + {
  134 + field: SchemaFiled.AGG,
  135 + label: '数据聚合功能',
  136 + component: 'Select',
  137 + componentProps: {
  138 + getPopupContainer: () => document.body,
  139 + options: [
  140 + { label: '最小值', value: AggregateDataEnum.MIN },
  141 + { label: '最大值', value: AggregateDataEnum.MAX },
  142 + { label: '平均值', value: AggregateDataEnum.AVG },
  143 + { label: '求和', value: AggregateDataEnum.SUM },
  144 + { label: '计数', value: AggregateDataEnum.COUNT },
  145 + { label: '空', value: AggregateDataEnum.NONE },
  146 + ],
  147 + },
  148 + },
  149 + {
  150 + field: SchemaFiled.INTERVAL,
  151 + label: '分组间隔',
  152 + component: 'Select',
  153 + dynamicRules: ({ model }) => {
  154 + return [
  155 + {
  156 + required: model[SchemaFiled.AGG] !== AggregateDataEnum.NONE,
  157 + message: '分组间隔为必填项',
  158 + type: 'number',
  159 + },
  160 + ];
  161 + },
  162 + ifShow({ values }) {
  163 + return values[SchemaFiled.AGG] !== AggregateDataEnum.NONE;
  164 + },
  165 + componentProps({ formModel, formActionType }) {
  166 + const options =
  167 + formModel[SchemaFiled.WAY] === QueryWay.LATEST
  168 + ? getPacketIntervalByValue(formModel[SchemaFiled.START_TS])
  169 + : getPacketIntervalByRange(formModel[SchemaFiled.DATE_RANGE]);
  170 + if (formModel[SchemaFiled.AGG] !== AggregateDataEnum.NONE) {
  171 + formActionType.setFieldsValue({ [SchemaFiled.LIMIT]: null });
  172 + }
  173 + return {
  174 + options,
  175 + getPopupContainer: () => document.body,
  176 + };
  177 + },
  178 + },
  179 + {
  180 + field: SchemaFiled.LIMIT,
  181 + label: '最大条数',
  182 + component: 'InputNumber',
  183 + ifShow({ values }) {
  184 + return values[SchemaFiled.AGG] === AggregateDataEnum.NONE;
  185 + },
  186 + helpMessage: ['根据查询条件,查出的数据条数不超过这个值'],
  187 + componentProps() {
  188 + return {
  189 + max: 50000,
  190 + min: 7,
  191 + getPopupContainer: () => document.body,
  192 + };
  193 + },
  194 + },
  195 + {
  196 + field: SchemaFiled.KEYS,
  197 + label: '设备属性',
  198 + component: 'Select',
  199 + componentProps: {
  200 + getPopupContainer: () => document.body,
  201 + },
  202 + },
  203 + ];
  204 +};
  205 +
  206 +export function getHistorySearchParams(value: Recordable) {
  207 + const { startTs, endTs, interval, agg, limit, way, keys, deviceId } = value;
  208 + if (way === QueryWay.LATEST) {
  209 + return {
  210 + keys,
  211 + entityId: deviceId,
  212 + startTs: moment().subtract(startTs, 'ms').valueOf(),
  213 + endTs: Date.now(),
  214 + interval,
  215 + agg,
  216 + limit,
  217 + };
  218 + } else {
  219 + return {
  220 + keys,
  221 + entityId: deviceId,
  222 + startTs: moment(startTs).valueOf(),
  223 + endTs: moment(endTs).valueOf(),
  224 + interval,
  225 + agg,
  226 + limit,
  227 + };
  228 + }
  229 +}
... ...
  1 +export { default as HistoryTrendModal } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { computed, nextTick, Ref, ref, unref } from 'vue';
  3 + import { getDeviceHistoryInfo } from '/@/api/alarm/position';
  4 + import { Empty, Spin } from 'ant-design-vue';
  5 + import { useECharts } from '/@/hooks/web/useECharts';
  6 + import { AggregateDataEnum } from '/@/views/device/localtion/config.data';
  7 + import { useGridLayout } from '/@/hooks/component/useGridLayout';
  8 + import { ColEx } from '/@/components/Form/src/types';
  9 + import { useForm, BasicForm } from '/@/components/Form';
  10 + import { formSchema, SchemaFiled } from './config';
  11 + import BasicModal from '/@/components/Modal/src/BasicModal.vue';
  12 + import { useModalInner } from '/@/components/Modal';
  13 + import { getAllDeviceByOrg } from '/@/api/dataBoard';
  14 + import { useHistoryData } from '/@/views/device/list/hook/useHistoryData';
  15 + import { BasicTable, useTable } from '/@/components/Table';
  16 + import { formatToDateTime } from '/@/utils/dateUtil';
  17 + import {
  18 + ModeSwitchButton,
  19 + TABLE_CHART_MODE_LIST,
  20 + EnumTableChartMode,
  21 + } from '/@/components/Widget';
  22 + import { ModalParamsType } from '/#/utils';
  23 + import { WidgetDataType } from '../../hooks/useDataSource';
  24 + import { ExtraDataSource } from '../../types';
  25 +
  26 + type DeviceOption = Record<'label' | 'value' | 'organizationId', string>;
  27 +
  28 + defineEmits(['register']);
  29 +
  30 + const mode = ref<EnumTableChartMode>(EnumTableChartMode.CHART);
  31 +
  32 + const chartRef = ref();
  33 +
  34 + const loading = ref(false);
  35 +
  36 + const isNull = ref(false);
  37 +
  38 + const historyData = ref<{ ts: number; value: string; name: string }[]>([]);
  39 +
  40 + const { deviceAttrs, getDeviceKeys, getSearchParams, setChartOptions, getDeviceAttribute } =
  41 + useHistoryData();
  42 +
  43 + const { setOptions, destory } = useECharts(chartRef as Ref<HTMLDivElement>);
  44 +
  45 + function hasDeviceAttr() {
  46 + return !!unref(deviceAttrs).length;
  47 + }
  48 +
  49 + const [register, method] = useForm({
  50 + schemas: formSchema(),
  51 + baseColProps: useGridLayout(2, 3, 4) as unknown as ColEx,
  52 + rowProps: {
  53 + gutter: 10,
  54 + },
  55 + labelWidth: 120,
  56 + fieldMapToTime: [
  57 + [SchemaFiled.DATE_RANGE, [SchemaFiled.START_TS, SchemaFiled.END_TS], 'YYYY-MM-DD HH:ss'],
  58 + ],
  59 + submitButtonOptions: {
  60 + loading: loading as unknown as boolean,
  61 + },
  62 + async submitFunc() {
  63 + search();
  64 + },
  65 + });
  66 +
  67 + const search = async () => {
  68 + // 表单验证
  69 + await method.validate();
  70 + const value = method.getFieldsValue();
  71 + const searchParams = getSearchParams(value);
  72 + if (!hasDeviceAttr()) return;
  73 + // 发送请求
  74 + loading.value = true;
  75 + const res = await getDeviceHistoryInfo(searchParams);
  76 + historyData.value = getTableHistoryData(res);
  77 + loading.value = false;
  78 + // 判断数据对象是否为空
  79 + if (!Object.keys(res).length) {
  80 + isNull.value = false;
  81 + return;
  82 + } else {
  83 + isNull.value = true;
  84 + }
  85 +
  86 + const selectedKeys = unref(deviceAttrs).find(
  87 + (item) => item.identifier === value[SchemaFiled.KEYS]
  88 + );
  89 + setOptions(setChartOptions(res, selectedKeys));
  90 + };
  91 +
  92 + const getTableHistoryData = (record: Recordable<{ ts: number; value: string }[]>) => {
  93 + const keys = Object.keys(record);
  94 + const list = keys.reduce((prev, next) => {
  95 + const list = record[next].map((item) => {
  96 + return {
  97 + ...item,
  98 + name: next,
  99 + };
  100 + });
  101 + return [...prev, ...list];
  102 + }, []);
  103 + return list;
  104 + };
  105 +
  106 + const getIdentifierNameMapping = computed(() => {
  107 + const mapping = {};
  108 + unref(deviceAttrs).forEach((item) => {
  109 + const { identifier, name } = item;
  110 + mapping[identifier] = name;
  111 + });
  112 + return mapping;
  113 + });
  114 +
  115 + const [registerTable] = useTable({
  116 + showIndexColumn: false,
  117 + showTableSetting: false,
  118 + dataSource: historyData,
  119 + maxHeight: 300,
  120 + size: 'small',
  121 + columns: [
  122 + {
  123 + title: '属性',
  124 + dataIndex: 'name',
  125 + format: (value) => {
  126 + return unref(getIdentifierNameMapping)[value];
  127 + },
  128 + },
  129 + {
  130 + title: '值',
  131 + dataIndex: 'value',
  132 + },
  133 + {
  134 + title: '更新时间',
  135 + dataIndex: 'ts',
  136 + format: (val) => {
  137 + return formatToDateTime(val, 'YYYY-MM-DD HH:mm:ss');
  138 + },
  139 + },
  140 + ],
  141 + });
  142 +
  143 + const getDeviceDataKey = async (record: DeviceOption) => {
  144 + const { organizationId, value } = record;
  145 + try {
  146 + const options = await getAllDeviceByOrg(organizationId);
  147 + const record = options.find((item) => item.tbDeviceId === value);
  148 + await getDeviceAttribute(record!);
  149 + await nextTick();
  150 + method.updateSchema({
  151 + field: SchemaFiled.KEYS,
  152 + componentProps: {
  153 + options: unref(deviceAttrs).map((item) => ({ label: item.name, value: item.identifier })),
  154 + },
  155 + });
  156 + } catch (error) {
  157 + throw error;
  158 + }
  159 + };
  160 +
  161 + const handleModalOpen = async () => {
  162 + await nextTick();
  163 +
  164 + method.setFieldsValue({
  165 + [SchemaFiled.START_TS]: 1 * 24 * 60 * 60 * 1000,
  166 + [SchemaFiled.LIMIT]: 7,
  167 + [SchemaFiled.AGG]: AggregateDataEnum.NONE,
  168 + });
  169 +
  170 + if (!hasDeviceAttr()) return;
  171 +
  172 + const { deviceId } = method.getFieldsValue();
  173 +
  174 + const res = await getDeviceHistoryInfo({
  175 + entityId: deviceId,
  176 + keys: unref(getDeviceKeys).join(),
  177 + startTs: Date.now() - 1 * 24 * 60 * 60 * 1000,
  178 + endTs: Date.now(),
  179 + agg: AggregateDataEnum.NONE,
  180 + limit: 7,
  181 + });
  182 + historyData.value = getTableHistoryData(res);
  183 + // 判断对象是否为空
  184 + if (!Object.keys(res).length) {
  185 + isNull.value = false;
  186 + return;
  187 + } else {
  188 + isNull.value = true;
  189 + }
  190 + setOptions(setChartOptions(res));
  191 + };
  192 +
  193 + const generateDeviceOptions = (dataSource: ExtraDataSource[]) => {
  194 + const record: { [key: string]: boolean } = {};
  195 +
  196 + const options: DeviceOption[] = [];
  197 + for (const item of dataSource) {
  198 + const { deviceName, gatewayDevice, slaveDeviceId, organizationId } = item;
  199 + let { deviceId } = item;
  200 + if (gatewayDevice && slaveDeviceId) {
  201 + deviceId = slaveDeviceId;
  202 + }
  203 + if (record[deviceId]) continue;
  204 + options.push({
  205 + label: deviceName,
  206 + value: deviceId,
  207 + organizationId,
  208 + });
  209 + record[deviceId] = true;
  210 + }
  211 +
  212 + return options;
  213 + };
  214 +
  215 + const [registerModal] = useModalInner(async (params: ModalParamsType<WidgetDataType>) => {
  216 + deviceAttrs.value = [];
  217 + loading.value = false;
  218 + const { record } = params;
  219 + const options = generateDeviceOptions(record.dataSource);
  220 + await nextTick();
  221 + method.updateSchema({
  222 + field: SchemaFiled.DEVICE_ID,
  223 + componentProps({ formActionType }) {
  224 + const { setFieldsValue } = formActionType;
  225 + return {
  226 + options,
  227 + onChange(_, record: DeviceOption) {
  228 + getDeviceDataKey(record);
  229 + setFieldsValue({ [SchemaFiled.KEYS]: null });
  230 + },
  231 + };
  232 + },
  233 + });
  234 +
  235 + if (options.length && options.at(0)?.value) {
  236 + const record = options.at(0)!;
  237 + await getDeviceDataKey(record);
  238 + try {
  239 + await method.setFieldsValue({ [SchemaFiled.DEVICE_ID]: record.value });
  240 + } catch (error) {}
  241 + }
  242 +
  243 + await handleModalOpen();
  244 + });
  245 +
  246 + const handleCancel = () => {
  247 + destory();
  248 + };
  249 +
  250 + const switchMode = (flag: EnumTableChartMode) => {
  251 + mode.value = flag;
  252 + };
  253 +</script>
  254 +
  255 +<template>
  256 + <BasicModal
  257 + @register="registerModal"
  258 + @cancel="handleCancel"
  259 + :destroy-on-close="true"
  260 + :show-ok-btn="false"
  261 + cancel-text="关闭"
  262 + width="70%"
  263 + title="历史趋势"
  264 + >
  265 + <section
  266 + class="flex flex-col p-4 h-full w-full min-w-7/10"
  267 + style="color: #f0f2f5; background-color: #f0f2f5"
  268 + >
  269 + <section class="bg-white my-3 p-2">
  270 + <BasicForm @register="register" />
  271 + </section>
  272 + <section class="bg-white p-3" style="min-height: 350px">
  273 + <Spin :spinning="loading" :absolute="true">
  274 + <div
  275 + v-show="mode === EnumTableChartMode.CHART"
  276 + class="flex h-70px items-center justify-end p-2"
  277 + >
  278 + <ModeSwitchButton
  279 + v-model:value="mode"
  280 + :mode="TABLE_CHART_MODE_LIST"
  281 + @change="switchMode"
  282 + />
  283 + </div>
  284 +
  285 + <div
  286 + v-show="isNull && mode === EnumTableChartMode.CHART"
  287 + ref="chartRef"
  288 + :style="{ height: '350px', width: '100%' }"
  289 + >
  290 + </div>
  291 + <Empty
  292 + v-if="mode === EnumTableChartMode.CHART"
  293 + class="h-350px flex flex-col justify-center items-center"
  294 + description="暂无数据,请选择设备查询"
  295 + v-show="!isNull"
  296 + />
  297 +
  298 + <BasicTable v-show="mode === EnumTableChartMode.TABLE" @register="registerTable">
  299 + <template #toolbar>
  300 + <div class="flex h-70px items-center justify-end p-2">
  301 + <ModeSwitchButton
  302 + v-model:value="mode"
  303 + :mode="TABLE_CHART_MODE_LIST"
  304 + @change="switchMode"
  305 + />
  306 + </div>
  307 + </template>
  308 + </BasicTable>
  309 + </Spin>
  310 + </section>
  311 + </section>
  312 + </BasicModal>
  313 +</template>
  314 +
  315 +<style scoped></style>
... ...
  1 +export { default as PageHeader } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { PageHeader } from 'ant-design-vue';
  3 + import { computed, unref } from 'vue';
  4 + import { useRoute, useRouter } from 'vue-router';
  5 + import { decode } from '../..';
  6 + import { RollbackOutlined } from '@ant-design/icons-vue';
  7 +
  8 + defineProps<{ widgetNumber: number }>();
  9 +
  10 + const ROUTE = useRoute();
  11 + const ROUTER = useRouter();
  12 + const getIsSharePage = computed(() => {
  13 + return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId');
  14 + });
  15 +
  16 + const getDataBoardName = computed(() => {
  17 + return decode((ROUTE.params as { boardName: string }).boardName || '');
  18 + });
  19 +
  20 + const handleBack = () => {
  21 + if (unref(getIsSharePage)) return;
  22 + ROUTER.go(-1);
  23 + };
  24 +
  25 + // const handleOpenCreatePanel = () => {};
  26 +</script>
  27 +
  28 +<template>
  29 + <PageHeader v-if="!getIsSharePage" class="pagerheader !px-5 !pt-5 !pb-0 !dark:bg-dark-700">
  30 + <template #title>
  31 + <div class="flex items-center h-18 px-4">
  32 + <RollbackOutlined
  33 + v-if="!getIsSharePage"
  34 + class="mr-3 cursor-pointer text-2xl"
  35 + @click="handleBack"
  36 + />
  37 + <span class="text-lg text-gray-700 dark:text-light-50">{{ getDataBoardName }}</span>
  38 + </div>
  39 + </template>
  40 + <template #extra>
  41 + <div class="w-full h-full flex justify-center items-center px-4">
  42 + <slot></slot>
  43 + </div>
  44 + </template>
  45 + <div>
  46 + <span class="mr-3 text-sm text-gray-500">已创建组件:</span>
  47 + <span class="text-blue-500"> {{ widgetNumber }}个</span>
  48 + </div>
  49 + </PageHeader>
  50 +</template>
  51 +
  52 +<style lang="less" scoped>
  53 + .pagerheader {
  54 + :deep(.ant-page-header-heading) {
  55 + @apply bg-light-50 dark:bg-dark-900;
  56 + }
  57 + }
  58 +</style>
... ...
  1 +export { default as WidgetDistribute } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { computed } from 'vue';
  3 + import { WidgetDataType } from '../../hooks/useDataSource';
  4 + import { fetchViewComponent } from '../../../packages';
  5 + import { ConfigType } from '/@/views/visual/packages/index.type';
  6 + import { ExtraDataSource } from '../../types';
  7 + import { CSSProperties } from 'vue';
  8 + import { toRaw } from 'vue';
  9 + import { Spin } from 'ant-design-vue';
  10 + import { componentMap, transformComponentKey } from '../../../packages/componentMap';
  11 + import { onMounted } from 'vue';
  12 + import { registerComponent } from '../../../packages/componentMap';
  13 + import { useGetComponentConfig } from '../../../packages/hook/useGetComponetConfig';
  14 + import { unref } from 'vue';
  15 + import { isBoolean } from '/@/utils/is';
  16 +
  17 + const props = defineProps<{
  18 + sourceInfo: WidgetDataType;
  19 + }>();
  20 +
  21 + const getLayout = computed(() => {
  22 + const { itemWidthRatio, itemHeightRatio } = props.sourceInfo || {};
  23 + return { width: `${itemWidthRatio}%`, height: `${itemHeightRatio}%` } as CSSProperties;
  24 + });
  25 +
  26 + const getCanRenderFlag = computed(() => {
  27 + const { itemHeightRatio, itemWidthRatio } = props.sourceInfo;
  28 + return [itemHeightRatio, itemWidthRatio].every(Boolean);
  29 + });
  30 +
  31 + const getComponentConfig = computed(() => {
  32 + const { widthPx, heightPx, itemWidthRatio, itemHeightRatio, frontId } = props.sourceInfo;
  33 +
  34 + return useGetComponentConfig(frontId, {
  35 + widthPx,
  36 + heightPx,
  37 + itemWidthRatio,
  38 + itemHeightRatio,
  39 + });
  40 + });
  41 +
  42 + const getBindValue = computed(() => {
  43 + return (options: ExtraDataSource) => {
  44 + const raw = toRaw(options);
  45 + const config = unref(getComponentConfig);
  46 + return {
  47 + config: {
  48 + ...config,
  49 + option: { ...config.option, ...raw },
  50 + },
  51 + };
  52 + };
  53 + });
  54 +
  55 + const getIsMultipleDataSourceComponent = computed(() => {
  56 + const { persetOption } = unref(getComponentConfig);
  57 + const { multipleDataSourceComponent } = persetOption || {};
  58 + return isBoolean(multipleDataSourceComponent) ? multipleDataSourceComponent : false;
  59 + });
  60 +
  61 + const getBindMultipleDataSourceValue = computed(() => {
  62 + const config = unref(getComponentConfig);
  63 + const { dataSource } = props.sourceInfo;
  64 + return {
  65 + config: {
  66 + ...config,
  67 + option: { ...config.option, dataSource: toRaw(dataSource) },
  68 + },
  69 + };
  70 + });
  71 +
  72 + onMounted(() => {
  73 + const key = transformComponentKey(props.sourceInfo.frontId);
  74 + const component = fetchViewComponent({ key } as ConfigType);
  75 + registerComponent(key, component);
  76 + });
  77 +</script>
  78 +
  79 +<template>
  80 + <Spin :spinning="!getCanRenderFlag" wrapperClassName="widget-distribute-spin">
  81 + <!-- 多数据源组件 -->
  82 + <template v-if="getCanRenderFlag">
  83 + <template v-if="getIsMultipleDataSourceComponent">
  84 + <component
  85 + :is="componentMap.get(transformComponentKey(props.sourceInfo.frontId))"
  86 + class="w-full h-full"
  87 + v-bind="getBindMultipleDataSourceValue"
  88 + />
  89 + </template>
  90 +
  91 + <!-- 单一数据源组件 -->
  92 + <template v-if="!getIsMultipleDataSourceComponent">
  93 + <component
  94 + v-for="item in props.sourceInfo.dataSource"
  95 + :key="item.uuid"
  96 + :is="componentMap.get(transformComponentKey(props.sourceInfo.frontId))"
  97 + :style="getLayout"
  98 + v-bind="getBindValue(item)"
  99 + />
  100 + </template>
  101 + </template>
  102 + </Spin>
  103 +</template>
  104 +
  105 +<style lang="less" scoped>
  106 + .widget-distribute-spin {
  107 + @apply w-full h-full overflow-hidden;
  108 +
  109 + :deep(.ant-spin-container) {
  110 + @apply w-full h-full flex flex-wrap;
  111 + }
  112 + }
  113 +</style>
... ...
  1 +<script lang="ts" setup>
  2 + import { ref } from 'vue';
  3 + import { useShare } from '../../hooks/useShare';
  4 + import AuthDropDown, { AuthDropMenuList } from '/@/components/Widget/AuthDropDown.vue';
  5 + import { useRole } from '/@/hooks/business/useRole';
  6 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  7 + import { VisualComponentPermission } from '../..';
  8 + import { Tooltip } from 'ant-design-vue';
  9 + import { MoreOutlined, AreaChartOutlined } from '@ant-design/icons-vue';
  10 + import { WidgetDataType } from '../../hooks/useDataSource';
  11 + import { useMessage } from '/@/hooks/web/useMessage';
  12 + import { addDataComponent, deleteDataComponent } from '/@/api/dataBoard';
  13 + import { useBoardId } from '../../hooks/useBoardId';
  14 + import { unref, toRaw } from 'vue';
  15 + import { ApiDataBoardDataType } from '../../types';
  16 + import { useCalcNewWidgetPosition } from '../../hooks/useCalcNewWidgetPosition';
  17 + import { Layout } from 'vue3-grid-layout';
  18 + import { DataComponentRecord } from '/@/api/dataBoard/model';
  19 +
  20 + const props = defineProps<{
  21 + sourceInfo: WidgetDataType;
  22 + rawDataSource: ApiDataBoardDataType;
  23 + }>();
  24 +
  25 + const emit = defineEmits<{
  26 + (event: 'ok'): void;
  27 + (event: 'update', data: WidgetDataType): void;
  28 + (event: 'openTrend', data: WidgetDataType): void;
  29 + }>();
  30 +
  31 + const { isCustomerUser } = useRole();
  32 +
  33 + const { getIsSharePage } = useShare();
  34 +
  35 + const { createMessage } = useMessage();
  36 +
  37 + const { boardId } = useBoardId();
  38 +
  39 + const dropMenuList = ref<AuthDropMenuList[]>([
  40 + {
  41 + text: '编辑组件',
  42 + event: DataActionModeEnum.UPDATE,
  43 + icon: 'ant-design:edit-outlined',
  44 + auth: VisualComponentPermission.UPDATE,
  45 + onClick: handleUpdate,
  46 + },
  47 + {
  48 + text: '复制组件',
  49 + event: DataActionModeEnum.COPY,
  50 + icon: 'ant-design:copy-outlined',
  51 + auth: VisualComponentPermission.COPY,
  52 + onClick: handleCopy,
  53 + },
  54 + {
  55 + text: '删除组件',
  56 + event: DataActionModeEnum.DELETE,
  57 + icon: 'ant-design:delete-outlined',
  58 + auth: VisualComponentPermission.DELETE,
  59 + onClick: handleDelete,
  60 + },
  61 + ]);
  62 +
  63 + function handleUpdate() {
  64 + emit('update', toRaw(props.sourceInfo));
  65 + }
  66 +
  67 + async function handleDelete() {
  68 + try {
  69 + await deleteDataComponent({ dataBoardId: unref(boardId), ids: [props.sourceInfo.id] });
  70 + createMessage.success('删除成功~');
  71 + emit('ok');
  72 + } catch (error) {}
  73 + }
  74 +
  75 + async function handleCopy() {
  76 + const id = props.sourceInfo.id;
  77 + const copyRecord = props.rawDataSource.componentData.find((item) => item.id === id);
  78 + const copyLayout = props.rawDataSource.componentLayout.find((item) => item.id === id);
  79 +
  80 + const raw = toRaw(copyRecord) as unknown as DataComponentRecord;
  81 +
  82 + const layout = useCalcNewWidgetPosition(
  83 + props.rawDataSource.componentLayout as unknown as Layout[],
  84 + { width: copyLayout!.w, height: copyLayout!.h }
  85 + );
  86 +
  87 + try {
  88 + await addDataComponent({
  89 + boardId: unref(boardId),
  90 + record: {
  91 + frontId: raw.frontId,
  92 + dataSource: raw.dataSource,
  93 + name: raw.name,
  94 + remark: raw.remark,
  95 + layout: layout as Layout,
  96 + },
  97 + });
  98 + createMessage.success('复制成功~');
  99 + emit('ok');
  100 + } catch (error) {
  101 + throw error;
  102 + }
  103 + }
  104 +
  105 + const handleOpenTrendModal = () => {
  106 + emit('openTrend', toRaw(props.sourceInfo));
  107 + };
  108 +</script>
  109 +
  110 +<template>
  111 + <section class="p-5 flex flex-col w-full">
  112 + <main class="flex w-full h-full h-7">
  113 + <Tooltip :title="sourceInfo.name">
  114 + <div class="flex-1 w-full h-full flex text-lg justify-center font-semibold">
  115 + {{ sourceInfo.name }}
  116 + </div>
  117 + </Tooltip>
  118 +
  119 + <div v-if="!getIsSharePage" class="flex items-center w-16 justify-evenly">
  120 + <Tooltip v-if="!isCustomerUser" title="趋势">
  121 + <AreaChartOutlined class="text-lg" @click="handleOpenTrendModal" />
  122 + </Tooltip>
  123 + <AuthDropDown
  124 + v-if="!isCustomerUser && dropMenuList.length"
  125 + :drop-menu-list="dropMenuList"
  126 + :trigger="['click']"
  127 + >
  128 + <Tooltip title="更多">
  129 + <MoreOutlined class="transform rotate-90 cursor-pointer text-lg" />
  130 + </Tooltip>
  131 + </AuthDropDown>
  132 + </div>
  133 + </main>
  134 + </section>
  135 +</template>
... ...
  1 +export { default as WidgetWrapper } from './index.vue';
  2 +export { default as WidgetHeader } from './WidgetHeader.vue';
... ...
  1 +<script lang="ts" setup></script>
  2 +
  3 +<template>
  4 + <section class="widget-wrapper !dark:bg-dark-900 flex flex-col bg-light-50 w-full h-full">
  5 + <!-- Header -->
  6 + <slot name="header"></slot>
  7 +
  8 + <!-- Container -->
  9 + <slot></slot>
  10 +
  11 + <!-- Footer -->
  12 + <slot name="footer"></slot>
  13 + </section>
  14 +</template>
... ...
  1 +import { computed } from 'vue';
  2 +import { useRoute } from 'vue-router';
  3 +
  4 +export const useBoardId = () => {
  5 + const ROUTE = useRoute();
  6 + const boardId = computed(() => {
  7 + return decodeURIComponent((ROUTE.params as Recordable).boardId as string);
  8 + });
  9 + return { boardId };
  10 +};
... ...
  1 +import { unref } from 'vue';
  2 +import { Layout } from 'vue3-grid-layout';
  3 +import { DEFAULT_MAX_COL, DEFAULT_WIDGET_HEIGHT, DEFAULT_WIDGET_WIDTH } from '..';
  4 +
  5 +interface GapRecord {
  6 + maxGap: number;
  7 + startIndex: Nullable<number>;
  8 + endIndex: Nullable<number>;
  9 +}
  10 +
  11 +export const useCalcNewWidgetPosition = (
  12 + layoutInfo: Layout[],
  13 + randomLayout = { width: DEFAULT_WIDGET_WIDTH, height: DEFAULT_WIDGET_HEIGHT }
  14 +) => {
  15 + let maxWidth = 0;
  16 + let maxHeight = 0;
  17 + let maxWidthRecord = {} as unknown as Layout;
  18 + let maxHeightRecord = {} as unknown as Layout;
  19 +
  20 + for (const item of unref(layoutInfo)) {
  21 + const { x, y, h, w } = item;
  22 + if (x + w > maxWidth) {
  23 + maxWidth = x + w;
  24 + maxWidthRecord = item;
  25 + }
  26 + if (y + h > maxHeight) {
  27 + maxHeight = y + h;
  28 + maxHeightRecord = item;
  29 + }
  30 + }
  31 +
  32 + maxWidth = maxWidthRecord.x + maxWidthRecord.w;
  33 + maxHeight = maxHeightRecord.y + maxHeightRecord.h;
  34 +
  35 + const array = Array.from({ length: maxHeight }, (_value) => {
  36 + return Array.from({ length: maxWidth });
  37 + });
  38 +
  39 + for (const item of layoutInfo) {
  40 + const { x, y, w, h } = item;
  41 +
  42 + for (let i = 0; i < h; i++) {
  43 + const rowIndex = y + i > array.length - 1 ? array.length - 1 : y + i;
  44 + const colEnd = x + w;
  45 + const row = array[rowIndex];
  46 + row.fill(true, x, colEnd);
  47 + }
  48 + }
  49 +
  50 + const checkAreaIsAvaliable = (rowIndex: number, rowRecord: GapRecord[]) => {
  51 + const { height } = randomLayout;
  52 +
  53 + for (const { startIndex: colStartIndex } of rowRecord) {
  54 + let record: GapRecord = { maxGap: 0, startIndex: null, endIndex: null };
  55 + const heightGapRecord: GapRecord[] = [];
  56 + for (let i = 0; i < height; i++) {
  57 + const rowStartIndex = rowIndex + i > array.length - 1 ? array.length - 1 : rowIndex + i;
  58 + const row = array[rowStartIndex];
  59 + const col = row[colStartIndex!];
  60 + if (col) {
  61 + if (record.maxGap > 0) heightGapRecord.push(record);
  62 + record = { maxGap: 0, startIndex: null, endIndex: null };
  63 + }
  64 + if (!col) {
  65 + record = {
  66 + maxGap: record.maxGap + 1,
  67 + startIndex: record.startIndex === null ? rowStartIndex : record.startIndex,
  68 + endIndex: rowStartIndex,
  69 + };
  70 + }
  71 + if (i + 1 === height) if (record.maxGap > 0) heightGapRecord.push(record);
  72 + }
  73 + const minHeight = heightGapRecord.length
  74 + ? Math.min(...heightGapRecord.map((item) => item.maxGap))
  75 + : 0;
  76 + if (minHeight >= height) {
  77 + let flag = true;
  78 + for (let colIndex = colStartIndex!; colIndex < record.endIndex!; colIndex++) {
  79 + for (let _rowIndex = rowIndex; _rowIndex < height; _rowIndex++) {
  80 + if (array[_rowIndex][colIndex]) {
  81 + flag = false;
  82 + break;
  83 + }
  84 + }
  85 + }
  86 + if (flag) return { y: rowIndex, x: colStartIndex!, flag: true };
  87 + }
  88 + }
  89 +
  90 + return { flag: false, x: 0, y: 0 };
  91 + };
  92 +
  93 + for (let rowIndex = 0; rowIndex < array.length; rowIndex++) {
  94 + const row = array[rowIndex];
  95 + let record: GapRecord = { maxGap: 0, startIndex: null, endIndex: null };
  96 + const widthGapRecord: GapRecord[] = [];
  97 +
  98 + const { width } = unref(randomLayout);
  99 +
  100 + for (let colIndex = 0; colIndex < DEFAULT_MAX_COL; colIndex++) {
  101 + const col = row[colIndex];
  102 + if (col) {
  103 + if (record.maxGap > 0) widthGapRecord.push(record);
  104 + record = { maxGap: 0, startIndex: null, endIndex: null };
  105 + }
  106 + if (!col) {
  107 + record = {
  108 + maxGap: record.maxGap + 1,
  109 + startIndex: record.startIndex === null ? colIndex : record.startIndex,
  110 + endIndex: colIndex,
  111 + };
  112 + }
  113 + if (colIndex + 1 === DEFAULT_MAX_COL) if (record.maxGap > 0) widthGapRecord.push(record);
  114 + }
  115 +
  116 + const maxWidth = widthGapRecord.length
  117 + ? Math.max(...widthGapRecord.map((item) => item.maxGap))
  118 + : 0;
  119 +
  120 + if (maxWidth >= width) {
  121 + const maxRecordList = widthGapRecord.filter((item) => item.maxGap >= maxWidth);
  122 + const { flag, x, y } = checkAreaIsAvaliable(rowIndex, maxRecordList);
  123 + if (flag) return { x, y, w: randomLayout.width, h: randomLayout.height };
  124 + }
  125 + }
  126 +
  127 + return { x: 0, y: array.length, w: randomLayout.width, h: randomLayout.height };
  128 +};
... ...
  1 +import { ComputedRef, computed, onMounted, ref, unref } from 'vue';
  2 +import { useRoute } from 'vue-router';
  3 +import { getDataComponent, updateDataBoardLayout } from '/@/api/dataBoard';
  4 +import { decode } from '../../board/config/config';
  5 +import { Layout } from 'vue3-grid-layout';
  6 +import {
  7 + ApiDataBoardDataType,
  8 + ApiDataBoardInfoType,
  9 + ComponentDataType,
  10 + ComponentLayoutType,
  11 +} from '../types';
  12 +import { buildUUID } from '/@/utils/uuid';
  13 +import { PublicComponentOptions } from '../../packages/index.type';
  14 +
  15 +export interface WidgetDataType extends Layout, ComponentDataType, PublicComponentOptions {}
  16 +
  17 +export const useDataSource = (propsRef: ComputedRef<Recordable>) => {
  18 + const ROUTE = useRoute();
  19 +
  20 + const loading = ref(false);
  21 +
  22 + const rawDataSource = ref<ApiDataBoardDataType>({} as ApiDataBoardDataType);
  23 +
  24 + const dataSource = ref<WidgetDataType[]>([]);
  25 +
  26 + const getSharePageData = computed(() => {
  27 + return unref(propsRef).value!;
  28 + });
  29 +
  30 + const getIsSharePage = computed(() => {
  31 + return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId');
  32 + });
  33 +
  34 + const getBoardId = computed(() => {
  35 + return decode((ROUTE.params as { boardId: string }).boardId);
  36 + });
  37 +
  38 + const getBasePageComponentData = async () => {
  39 + try {
  40 + return (await getDataComponent(unref(getBoardId))) as unknown as ApiDataBoardInfoType;
  41 + } catch (error) {}
  42 + return {} as ApiDataBoardInfoType;
  43 + };
  44 +
  45 + const getDataBoradDetail = async () => {
  46 + try {
  47 + return unref(getIsSharePage)
  48 + ? (unref(getSharePageData) as ApiDataBoardInfoType)
  49 + : await getBasePageComponentData();
  50 + } catch (error) {
  51 + return {} as ApiDataBoardInfoType;
  52 + }
  53 + };
  54 +
  55 + /**
  56 + * @description 获取数据源
  57 + */
  58 + const getDataSource = async () => {
  59 + try {
  60 + loading.value = true;
  61 + const { data } = await getDataBoradDetail();
  62 + rawDataSource.value = data;
  63 + const result: WidgetDataType[] = data.componentLayout.map((item) => {
  64 + const { id } = item;
  65 + const dataSource =
  66 + data.componentData.find((item) => item.id == id) || ({} as ComponentDataType);
  67 +
  68 + dataSource.dataSource = dataSource.dataSource.map((item) => ({
  69 + ...item,
  70 + uuid: buildUUID(),
  71 + }));
  72 + return {
  73 + i: buildUUID(),
  74 + ...item,
  75 + ...dataSource,
  76 + } as WidgetDataType;
  77 + });
  78 + dataSource.value = result;
  79 + return result;
  80 + } catch (error) {
  81 + throw error;
  82 + } finally {
  83 + loading.value = false;
  84 + }
  85 + };
  86 +
  87 + /**
  88 + * @description 获取页面中的所有组件坐标
  89 + */
  90 + const getLayoutInfo = () => {
  91 + return unref(dataSource).map((item) => {
  92 + return {
  93 + id: item.id,
  94 + h: item.h,
  95 + w: item.w,
  96 + x: item.x,
  97 + y: item.y,
  98 + } as ComponentLayoutType;
  99 + });
  100 + };
  101 +
  102 + /**
  103 + * @description 更新坐标
  104 + */
  105 + const setLayoutInfo = async () => {
  106 + try {
  107 + await updateDataBoardLayout({
  108 + boardId: unref(getBoardId),
  109 + layout: getLayoutInfo(),
  110 + });
  111 + } catch (error) {}
  112 + };
  113 +
  114 + onMounted(() => {
  115 + getDataSource();
  116 + });
  117 +
  118 + return {
  119 + loading,
  120 + draggable: !unref(getIsSharePage),
  121 + resizable: !unref(getIsSharePage),
  122 + dataSource,
  123 + rawDataSource,
  124 + getDataSource,
  125 + setLayoutInfo,
  126 + };
  127 +};
... ...
  1 +import { throttle } from 'lodash-es';
  2 +import { Ref, unref } from 'vue';
  3 +import { WidgetDataType } from './useDataSource';
  4 +
  5 +export const useDragGridLayout = (
  6 + dataSourceRef: Ref<WidgetDataType[]>,
  7 + setLayoutInfo: () => Promise<void>
  8 +) => {
  9 + const findUpdateItem = (i: string) => unref(dataSourceRef).find((item) => item.i === i);
  10 +
  11 + const calcLayout = (i: string, newHPx: number, newWPx: number) => {
  12 + const record = findUpdateItem(i);
  13 +
  14 + if (!record) return;
  15 +
  16 + const length = record.dataSource.length || 0;
  17 +
  18 + const row = Math.floor(Math.pow(length, 0.5));
  19 + const col = Math.floor(length / row);
  20 + let width = Number((100 / col).toFixed(2));
  21 + let height = Number((100 / row).toFixed(2));
  22 +
  23 + const WHRatio = newWPx / newHPx;
  24 + const HWRatio = newHPx / newWPx;
  25 +
  26 + if (WHRatio > 1.6) {
  27 + width = Number((100 / length).toFixed(2));
  28 + height = 100;
  29 + }
  30 +
  31 + if (HWRatio > 1.6) {
  32 + height = Number((100 / length).toFixed(2));
  33 + width = 100;
  34 + }
  35 +
  36 + const widgetWrapperHeader = 68;
  37 + // const widgetWrapperTitle = 40;
  38 + const widgetWrapperTitle = 0;
  39 + let offsetHeight = widgetWrapperHeader;
  40 + if (record.name) {
  41 + offsetHeight = offsetHeight + widgetWrapperTitle;
  42 + }
  43 +
  44 + record.widthPx = newWPx;
  45 + record.heightPx = newHPx - offsetHeight;
  46 + record.itemWidthRatio = width;
  47 + record.itemHeightRatio = height;
  48 + };
  49 +
  50 + const resized = (_i: string, _newH: number, _newW: number, _newHPx: string, _newWPx: string) => {
  51 + setLayoutInfo();
  52 + };
  53 +
  54 + const resize = throttle(
  55 + (i: string, _newH: number, _newW: number, newHPx: number, newWPx: number) => {
  56 + calcLayout(i, Number(newHPx), Number(newWPx));
  57 + },
  58 + 50
  59 + );
  60 +
  61 + const moved = (_i: string, _newX: number, _newY: number) => {
  62 + setLayoutInfo();
  63 + };
  64 +
  65 + const containerResized = (
  66 + i: string,
  67 + _newH: number,
  68 + _newW: number,
  69 + newHPx: string,
  70 + newWPx: string
  71 + ) => {
  72 + calcLayout(i, Number(newHPx), Number(newWPx));
  73 + };
  74 +
  75 + return {
  76 + resize,
  77 + containerResized,
  78 + moved,
  79 + resized,
  80 + };
  81 +};
... ...
  1 +import { computed } from 'vue';
  2 +import { useRoute } from 'vue-router';
  3 +
  4 +export const useShare = () => {
  5 + const ROUTE = useRoute();
  6 + const getIsSharePage = computed(() => {
  7 + return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId');
  8 + });
  9 +
  10 + return { getIsSharePage };
  11 +};
... ...
  1 +export { default as Palette } from './index.vue';
  2 +
  3 +export const paletteInitSize = {
  4 + w: 1920,
  5 + h: 1080,
  6 +};
  7 +
  8 +export const DEFAULT_MAX_COL = 24;
  9 +export const DEFAULT_WIDGET_WIDTH = 6;
  10 +export const DEFAULT_WIDGET_HEIGHT = 6;
  11 +export const DEFAULT_MIN_HEIGHT = 5;
  12 +export const DEFAULT_MIN_WIDTH = 3;
  13 +export const DEFAULT_ITEM_MARIGN = 20;
  14 +
  15 +import { ViewTypeEnum } from '/@/views/sys/share/config/config';
  16 +
  17 +export enum MoreActionEvent {
  18 + EDIT = 'edit',
  19 + COPY = 'copy',
  20 + DELETE = 'delete',
  21 + SHARE = 'share',
  22 +}
  23 +
  24 +export enum VisualBoardPermission {
  25 + UPDATE = 'api:yt:data_board:update:update',
  26 + DELETE = 'api:yt:data_board:delete',
  27 + CREATE = '',
  28 + DETAIL = 'api:yt:data_component:list',
  29 +}
  30 +
  31 +export enum VisualComponentPermission {
  32 + UPDATE = 'api:yt:data_component:update:update',
  33 + DELETE = 'api:yt:data_component:delete',
  34 + COPY = 'api:yt:dataBoardDetail:copy',
  35 + CREATE = 'api:yt:data_component:add:post',
  36 +}
  37 +
  38 +export const DATA_BOARD_SHARE_URL = (id: string, publicId: string) =>
  39 + `/share/${ViewTypeEnum.DATA_BOARD}/${id}/${publicId}`;
  40 +
  41 +export const isBataBoardSharePage = (url: string) => {
  42 + const reg = /^\/data\/board\/share/g;
  43 + return reg.test(url);
  44 +};
  45 +
  46 +export const encode = (string: string) => {
  47 + return encodeURIComponent(string);
  48 +};
  49 +
  50 +export const decode = (string: string) => {
  51 + return decodeURIComponent(string);
  52 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { onMounted, unref } from 'vue';
  3 + import { ref } from 'vue';
  4 + import { Empty, Spin, Button } from 'ant-design-vue';
  5 + import { getBoundingClientRect } from '/@/utils/domUtils';
  6 + import { GridItem, GridLayout } from 'vue3-grid-layout';
  7 + import {
  8 + DEFAULT_MAX_COL,
  9 + DEFAULT_MIN_HEIGHT,
  10 + DEFAULT_MIN_WIDTH,
  11 + DEFAULT_ITEM_MARIGN,
  12 + VisualComponentPermission,
  13 + } from './index';
  14 + import { useDragGridLayout } from './hooks/useDragGridLayout';
  15 + import { WidgetHeader, WidgetWrapper } from './components/WidgetWrapper';
  16 + import { computed } from 'vue';
  17 + import { WidgetDataType, useDataSource } from './hooks/useDataSource';
  18 + import { WidgetDistribute } from './components/WidgetDistribute';
  19 + import { DataSourceBindPanel } from '../dataSourceBindPanel';
  20 + import { PageHeader } from './components/PagerHeader';
  21 + import { useShare } from './hooks/useShare';
  22 + import { useRole } from '/@/hooks/business/useRole';
  23 + import { Authority } from '/@/components/Authority';
  24 + import { useModal } from '/@/components/Modal';
  25 + import { ModalParamsType } from '/#/utils';
  26 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  27 + import { HistoryTrendModal } from './components/HistoryTrendModal';
  28 + import { useSocket } from '../packages/hook/useSocket';
  29 + import { watch } from 'vue';
  30 +
  31 + const props = defineProps<{
  32 + value?: Recordable;
  33 + }>();
  34 +
  35 + const getProps = computed(() => props);
  36 +
  37 + const containerRefEl = ref<Nullable<HTMLDivElement>>(null);
  38 +
  39 + const containerRectRef = ref<DOMRect>({} as unknown as DOMRect);
  40 +
  41 + const { loading, draggable, resizable, dataSource, rawDataSource, setLayoutInfo, getDataSource } =
  42 + useDataSource(getProps);
  43 +
  44 + const { resize, resized, moved, containerResized } = useDragGridLayout(dataSource, setLayoutInfo);
  45 +
  46 + const [register, { openModal }] = useModal();
  47 +
  48 + const [registerTrendModal, { openModal: openTrendModal }] = useModal();
  49 +
  50 + /**
  51 + * @description 获取画板宽高
  52 + */
  53 + onMounted(() => {
  54 + const rect = getBoundingClientRect(unref(containerRefEl)!);
  55 + if (rect) {
  56 + containerRectRef.value = rect as DOMRect;
  57 + }
  58 + });
  59 + const { getIsSharePage } = useShare();
  60 + const { isCustomerUser } = useRole();
  61 + const handleOpenCreatePanel = () => {
  62 + openModal(true, { mode: DataActionModeEnum.CREATE } as ModalParamsType);
  63 + };
  64 +
  65 + const handleUpdateWidget = (data: WidgetDataType) => {
  66 + openModal(true, { mode: DataActionModeEnum.UPDATE, record: data } as ModalParamsType);
  67 + };
  68 +
  69 + const handleOpenTrend = (data: WidgetDataType) => {
  70 + openTrendModal(true, { mode: DataActionModeEnum.READ, record: data } as ModalParamsType);
  71 + };
  72 +
  73 + useSocket(dataSource);
  74 +
  75 + watch(
  76 + getIsSharePage,
  77 + (value) => {
  78 + if (value) {
  79 + const root = document.querySelector('#app');
  80 + (root as HTMLDivElement).style.backgroundColor = '#F5F5F5';
  81 + }
  82 + },
  83 + { immediate: true }
  84 + );
  85 +</script>
  86 +
  87 +<template>
  88 + <section
  89 + ref="containerRefEl"
  90 + class="palette w-full h-full flex-col bg-neutral-100 flex dark:bg-dark-700 dark:text-light-50"
  91 + >
  92 + <PageHeader :widget-number="dataSource.length">
  93 + <Authority :value="VisualComponentPermission.CREATE">
  94 + <Button
  95 + v-if="!getIsSharePage && !isCustomerUser"
  96 + type="primary"
  97 + @click="handleOpenCreatePanel"
  98 + >
  99 + 创建组件
  100 + </Button>
  101 + </Authority>
  102 + </PageHeader>
  103 +
  104 + <Spin :spinning="loading">
  105 + <GridLayout
  106 + v-model:layout="dataSource"
  107 + :col-num="DEFAULT_MAX_COL"
  108 + :row-height="30"
  109 + :margin="[DEFAULT_ITEM_MARIGN, DEFAULT_ITEM_MARIGN]"
  110 + :is-draggable="draggable"
  111 + :is-resizable="resizable"
  112 + :vertical-compact="true"
  113 + :use-css-transforms="true"
  114 + style="width: 100%"
  115 + >
  116 + <GridItem
  117 + v-for="item in dataSource"
  118 + :key="item.i"
  119 + :static="item.static"
  120 + :x="item.x"
  121 + :y="item.y"
  122 + :w="item.w"
  123 + :h="item.h"
  124 + :i="item.i"
  125 + :min-h="DEFAULT_MIN_HEIGHT"
  126 + :min-w="DEFAULT_MIN_WIDTH"
  127 + :style="{ display: 'flex', flexWrap: 'wrap' }"
  128 + class="grid-item-layout"
  129 + @resized="resized"
  130 + @resize="resize"
  131 + @moved="moved"
  132 + @container-resized="containerResized"
  133 + drag-ignore-from=".no-drag"
  134 + >
  135 + <WidgetWrapper>
  136 + <template #header>
  137 + <WidgetHeader
  138 + :raw-data-source="rawDataSource"
  139 + :source-info="item"
  140 + @update="handleUpdateWidget"
  141 + @open-trend="handleOpenTrend"
  142 + @ok="getDataSource"
  143 + />
  144 + </template>
  145 + <WidgetDistribute :source-info="item" />
  146 + </WidgetWrapper>
  147 + </GridItem>
  148 + </GridLayout>
  149 + <Empty
  150 + v-if="!dataSource.length"
  151 + class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
  152 + />
  153 + </Spin>
  154 +
  155 + <DataSourceBindPanel @register="register" :layout="dataSource" @ok="getDataSource" />
  156 +
  157 + <HistoryTrendModal @register="registerTrendModal" />
  158 + </section>
  159 +</template>
  160 +
  161 +<style lang="less" scoped></style>
... ...
  1 +import { PublicComponentOptions } from '../../packages/index.type';
  2 +
  3 +export interface ComponentDataType<T = ExtraDataSource> {
  4 + id: string;
  5 + creator: string;
  6 + createTime: string;
  7 + enabled: boolean;
  8 + tenantId: string;
  9 + dataBoardId: string;
  10 + frontId: string;
  11 + dataSource: T[];
  12 + name?: string;
  13 + remark?: string;
  14 +}
  15 +
  16 +export interface DataSource {
  17 + deviceProfileId: string;
  18 + organizationId: string;
  19 + deviceId: string;
  20 + deviceType: string;
  21 + attribute: string;
  22 + deviceName: string;
  23 + gatewayDevice: boolean;
  24 + slaveDeviceId: string;
  25 + deviceRename: string;
  26 + attributeRename: string;
  27 + componentInfo: ComponentInfo;
  28 + customCommand: CustomCommand;
  29 +}
  30 +
  31 +export interface ExtraDataSource extends DataSource, PublicComponentOptions {
  32 + uuid: string;
  33 +}
  34 +
  35 +export interface ComponentInfoGradientInfoType {
  36 + key: string;
  37 + value: number;
  38 + color: string;
  39 +}
  40 +
  41 +export interface FlowmeterConfigType {
  42 + backgroundColor: string;
  43 + waveFirst: string;
  44 + waveSecond: string;
  45 + waveThird: string;
  46 +}
  47 +
  48 +export interface ComponentInfo {
  49 + fontColor: string;
  50 + unit: string;
  51 + icon: string;
  52 + iconColor: string;
  53 + showDeviceName: boolean;
  54 + gradientInfo: ComponentInfoGradientInfoType[];
  55 + flowmeterConfig: FlowmeterConfigType;
  56 +}
  57 +
  58 +export interface CustomCommand {
  59 + transportType: string;
  60 + commandType: string;
  61 + command: string;
  62 + service: string;
  63 +}
  64 +
  65 +export interface ComponentLayoutType {
  66 + x: number;
  67 + y: number;
  68 + w: number;
  69 + h: number;
  70 + id: string;
  71 +}
  72 +
  73 +export interface ApiDataBoardDataType {
  74 + componentData: ComponentDataType[];
  75 + componentLayout: ComponentLayoutType[];
  76 +}
  77 +
  78 +export interface ApiDataBoardInfoType {
  79 + data: ApiDataBoardDataType;
  80 +}
... ...
  1 +export { default as StageFrame } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { Card } from 'ant-design-vue';
  3 + // :style="{ borderColor: props.controlId === props.checkedId ? '#3079FF' : '#fff' }"
  4 +</script>
  5 +
  6 +<template>
  7 + <Card
  8 + hoverable
  9 + bordered
  10 + class="strage-frame border-2 widget-select !bg-light-50 dark:bg-dark-400 cursor-pointer"
  11 + >
  12 + <div class="stage-frame-container w-full h-full justify-center items-center w-60 h-50">
  13 + <slot></slot>
  14 + </div>
  15 + <Card.Meta>
  16 + <template #description>
  17 + <slot name="description"></slot>
  18 + </template>
  19 + </Card.Meta>
  20 + </Card>
  21 +</template>
  22 +
  23 +<style lang="less" scoped>
  24 + .strage-frame {
  25 + @apply flex justify-center items-center flex-col box-border border-light-50 dark:border-gray-700;
  26 +
  27 + box-shadow: 0 1px 10px 0 #0000001a !important;
  28 + width: 244px;
  29 + height: 244px;
  30 +
  31 + :deep(.ant-card-body) {
  32 + @apply p-0 dark:bg-dark-900;
  33 + }
  34 +
  35 + :deep(.ant-card-meta) {
  36 + @apply m-0 leading-10;
  37 + }
  38 + }
  39 +</style>
... ...
  1 +export { default as WidgetLibrary } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { StageFrame } from './components/StageFrame';
  3 + import { packageList } from '../packages/package';
  4 + import { computed, ref } from 'vue';
  5 + import { Tabs } from 'ant-design-vue';
  6 + import {
  7 + ConfigType,
  8 + PackagesCategoryEnum,
  9 + PackagesCategoryNameEnum,
  10 + } from '../packages/index.type';
  11 + import { fetchViewComponent, createComponent } from '../packages';
  12 + import { componentMap, registerComponent } from '../packages/componentMap';
  13 + import { watchEffect } from 'vue';
  14 + import { unref } from 'vue';
  15 + import { watch } from 'vue';
  16 +
  17 + interface PropsCheckedType {
  18 + componentKey: string;
  19 + categoryKey: string;
  20 + }
  21 +
  22 + const props = defineProps<{
  23 + checked?: PropsCheckedType;
  24 + }>();
  25 +
  26 + const emit = defineEmits<{
  27 + (event: 'update:checked', value: PropsCheckedType): void;
  28 + }>();
  29 +
  30 + const activeKey = ref(PackagesCategoryEnum.TEXT);
  31 +
  32 + const getCategory = computed(() => {
  33 + return Object.keys(packageList).map((key) => {
  34 + const category = packageList[key] as ConfigType[];
  35 + return {
  36 + title: PackagesCategoryNameEnum[PackagesCategoryEnum[key]],
  37 + key,
  38 + items: category,
  39 + };
  40 + });
  41 + });
  42 +
  43 + const getBindConfig = (key: string) => {
  44 + const config = createComponent({ key } as ConfigType);
  45 + return {
  46 + config,
  47 + };
  48 + };
  49 +
  50 + watchEffect(() => {
  51 + unref(getCategory).forEach((category) => {
  52 + category.items.forEach((item) => {
  53 + const { key } = item;
  54 + const component = fetchViewComponent({ key } as ConfigType);
  55 + registerComponent(key, component);
  56 + });
  57 + });
  58 + });
  59 +
  60 + const handleSelected = (item: ConfigType) => {
  61 + const { key, package: packageKey } = item;
  62 + emit('update:checked', { categoryKey: packageKey!, componentKey: key });
  63 + };
  64 +
  65 + watch(
  66 + () => props.checked?.categoryKey,
  67 + (value) => {
  68 + if (value) activeKey.value = value as PackagesCategoryEnum;
  69 + }
  70 + );
  71 +</script>
  72 +
  73 +<template>
  74 + <section>
  75 + <Tabs v-model:activeKey="activeKey">
  76 + <Tabs.TabPane
  77 + v-for="item in getCategory"
  78 + :key="item.key"
  79 + :tab="item.title"
  80 + :forceRender="false"
  81 + >
  82 + <main class="flex min-h-64 px-2 gap-8 flex-wrap justify-start">
  83 + <StageFrame
  84 + class="mt-4"
  85 + v-for="temp in item.items"
  86 + :key="temp.key"
  87 + @click="handleSelected(temp)"
  88 + :class="temp.key === props.checked?.componentKey ? '!border-2 !border-blue-500' : ''"
  89 + >
  90 + <component :is="componentMap.get(temp.key)" v-bind="getBindConfig(temp.key)" />
  91 + <template #description>
  92 + <div class="h-10 leading-10 text-center border-t border-light-600 border-solid">
  93 + {{ temp.title }}
  94 + </div>
  95 + </template>
  96 + </StageFrame>
  97 + </main>
  98 + </Tabs.TabPane>
  99 + </Tabs>
  100 + </section>
  101 +</template>
... ...