Commit d50cd71650ff3deab3e81b7daea89993b45a84d9

Authored by ww
1 parent 828a7a79

refactor: 重构数据看板

Showing 67 changed files with 4066 additions and 601 deletions

Too many changes to show.

To preserve performance only 67 of 144 files are displayed.

... ... @@ -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 { 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 +};
... ...