Commit 239c2b3dab312449724b1743149be9e8761ccdcf

Authored by ww
1 parent ba789ed2

feat: 实现过滤器所有节点

Showing 42 changed files with 1388 additions and 132 deletions
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 "inited", 16 "inited",
17 "liveui", 17 "liveui",
18 "MQTT", 18 "MQTT",
  19 + "noconflict",
19 "notif", 20 "notif",
20 "PROTOBUF", 21 "PROTOBUF",
21 "rtsp", 22 "rtsp",
@@ -58,6 +58,7 @@ @@ -58,6 +58,7 @@
58 "flv.js": "^1.6.2", 58 "flv.js": "^1.6.2",
59 "hls.js": "^1.0.10", 59 "hls.js": "^1.0.10",
60 "intro.js": "^4.1.0", 60 "intro.js": "^4.1.0",
  61 + "js-beautify": "^1.14.9",
61 "jsoneditor": "^9.7.2", 62 "jsoneditor": "^9.7.2",
62 "jwt-decode": "^3.1.2", 63 "jwt-decode": "^3.1.2",
63 "lodash-es": "^4.17.21", 64 "lodash-es": "^4.17.21",
  1 +import { DeviceInfoItemType } from './model';
  2 +import { TBPaginationResult } from '/#/axios';
  3 +import { defHttp } from '/@/utils/http/axios';
  4 +
  5 +enum Api {
  6 + GET_DEVICE_INFOS = '/tenant/deviceInfos',
  7 +}
  8 +
  9 +export const getDeviceInfos = () => {
  10 + return defHttp.get<TBPaginationResult<DeviceInfoItemType>>(
  11 + {
  12 + url: Api.GET_DEVICE_INFOS,
  13 + },
  14 + { joinPrefix: false }
  15 + );
  16 +};
  1 +export interface DeviceInfoItemType {
  2 + id: Id;
  3 + createdTime: number;
  4 + additionalInfo: AdditionalInfo;
  5 + tenantId: Id;
  6 + customerId: Id;
  7 + name: string;
  8 + type: string;
  9 + label: string;
  10 + deviceProfileId: Id;
  11 + deviceData: DeviceData;
  12 + firmwareId: any;
  13 + softwareId: any;
  14 + customerTitle: any;
  15 + customerIsPublic: boolean;
  16 + deviceProfileName: string;
  17 +}
  18 +
  19 +export interface Id {
  20 + entityType: string;
  21 + id: string;
  22 +}
  23 +
  24 +export interface DeviceData {
  25 + configuration: Configuration;
  26 + transportConfiguration: TransportConfiguration;
  27 +}
  28 +
  29 +export interface AdditionalInfo {
  30 + gateway: boolean;
  31 + description: string;
  32 + overwriteActivityTime: boolean;
  33 +}
  34 +export interface Configuration {
  35 + type: string;
  36 +}
  37 +
  38 +export interface TransportConfiguration {
  39 + type: string;
  40 +}
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 - import { ref, watch } from 'vue';  
3 - import JSONEditor, { JSONEditorOptions } from 'jsoneditor';  
4 - import 'jsoneditor/dist/jsoneditor.min.css';  
5 - import { unref } from 'vue';  
6 - import { onMounted } from 'vue';  
7 - import { computed } from '@vue/reactivity';  
8 - import { onUnmounted } from 'vue'; 2 + import { onMounted, computed, onUnmounted, unref, ref, watch } from 'vue';
  3 + import { Tooltip } from 'ant-design-vue';
  4 + import { Icon } from '/@/components/Icon';
  5 + import { useFullscreen } from '@vueuse/core';
  6 + import { isNumber, isObject, isString } from '/@/utils/is';
  7 + import AceEditor, { Ace } from 'ace-builds';
  8 + import workerJsonUrl from 'ace-builds/src-noconflict/worker-json?url';
  9 + import githubTheme from 'ace-builds/src-noconflict/theme-github?url';
  10 + import 'ace-builds/src-noconflict/mode-json';
9 11
10 enum EventEnum { 12 enum EventEnum {
11 UPDATE_VALUE = 'update:value', 13 UPDATE_VALUE = 'update:value',
@@ -16,89 +18,133 @@ @@ -16,89 +18,133 @@
16 18
17 const props = withDefaults( 19 const props = withDefaults(
18 defineProps<{ 20 defineProps<{
19 - value?: string;  
20 - options?: JSONEditorOptions;  
21 - height?: number; 21 + value?: string | Recordable;
  22 + height?: number | string;
  23 + title?: string;
  24 + disabled?: boolean;
22 }>(), 25 }>(),
23 { 26 {
24 - options: () =>  
25 - ({  
26 - mode: 'code',  
27 - mainMenuBar: false,  
28 - statusBar: false,  
29 - } as JSONEditorOptions),  
30 height: 150, 27 height: 150,
31 } 28 }
32 ); 29 );
33 30
34 const emit = defineEmits<{ 31 const emit = defineEmits<{
35 - (e: EventEnum.UPDATE_VALUE, value: any, instance?: JSONEditor): void;  
36 - (e: EventEnum.CHANGE, value: any, instance?: JSONEditor): void;  
37 - (e: EventEnum.BLUR, event: Event, instance?: JSONEditor): void;  
38 - (e: EventEnum.FOCUS, event: Event, instance?: JSONEditor): void; 32 + (e: EventEnum.UPDATE_VALUE, value: any, instance?: Ace.Editor): void;
  33 + (e: EventEnum.CHANGE, value: any, instance?: Ace.Editor): void;
  34 + (e: EventEnum.BLUR, event: Event, instance?: Ace.Editor): void;
  35 + (e: EventEnum.FOCUS, event: Event, instance?: Ace.Editor): void;
39 }>(); 36 }>();
40 37
41 const jsonEditorElRef = ref<Nullable<any>>(); 38 const jsonEditorElRef = ref<Nullable<any>>();
42 39
43 - const editoreRef = ref<JSONEditor>(); 40 + const editoreRef = ref<Ace.Editor>();
44 41
45 const isFocus = ref(false); 42 const isFocus = ref(false);
46 43
47 - const handleChange = (value: any) => { 44 + const handleOnChange = () => {
  45 + const value = get();
48 emit(EventEnum.UPDATE_VALUE, value, unref(editoreRef)); 46 emit(EventEnum.UPDATE_VALUE, value, unref(editoreRef));
49 emit(EventEnum.CHANGE, value, unref(editoreRef)); 47 emit(EventEnum.CHANGE, value, unref(editoreRef));
50 }; 48 };
51 49
52 - const handleEmit = (event: Event, key: EventEnum) => {  
53 - emit(key as EventEnum[keyof EventEnum], event, unref(editoreRef)); 50 + const handleOnBlur = (event: Event) => {
  51 + isFocus.value = false;
  52 + emit(EventEnum.BLUR, event, unref(editoreRef));
54 }; 53 };
55 54
56 - const getOptions = computed(() => {  
57 - const { options } = props;  
58 - return {  
59 - ...options,  
60 - onChangeText: handleChange,  
61 - onBlur: (event: Event) => {  
62 - isFocus.value = false;  
63 - handleEmit(event, EventEnum.BLUR);  
64 - },  
65 - onFocus: (event: Event) => {  
66 - isFocus.value = true;  
67 - handleEmit(event, EventEnum.FOCUS);  
68 - },  
69 - } as JSONEditorOptions;  
70 - }); 55 + const handleOnFocus = (event: Event) => {
  56 + isFocus.value = true;
  57 + emit(EventEnum.FOCUS, event, unref(editoreRef));
  58 + };
  59 +
  60 + const getFormatValue = (value: Recordable | string = '') => {
  61 + return isObject(value) ? JSON.stringify(value, null, 2) : value;
  62 + };
71 63
72 const initialize = () => { 64 const initialize = () => {
73 - editoreRef.value = new JSONEditor(unref(jsonEditorElRef), unref(getOptions)); 65 + AceEditor.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl);
  66 + AceEditor.config.setModuleUrl('ace/theme/github', githubTheme);
  67 + const editor = AceEditor.edit(unref(jsonEditorElRef)!, {
  68 + mode: 'ace/mode/json',
  69 + });
  70 + editor.setTheme('ace/theme/github');
  71 + editor.setOptions({
  72 + fontSize: 14,
  73 + });
  74 +
  75 + editor.on('change', handleOnChange);
  76 + editor.on('blur', handleOnBlur);
  77 + editor.on('focus', handleOnFocus);
  78 +
  79 + editoreRef.value = editor;
  80 + unref(editoreRef)?.setValue(getFormatValue(props.value), 1);
  81 + unref(editoreRef)?.setReadOnly(props.disabled);
74 }; 82 };
75 83
76 watch( 84 watch(
77 () => props.value, 85 () => props.value,
78 (target) => { 86 (target) => {
79 - if (unref(isFocus)) return;  
80 - unref(editoreRef)?.setText(target || ''); 87 + // const position = unref(editoreRef)?.getCursorPosition();
  88 + unref(editoreRef)?.setValue(getFormatValue(target));
  89 + unref(editoreRef)?.clearSelection();
  90 + // position && unref(editoreRef)?.moveCursorToPosition(position!);
81 }, 91 },
82 { 92 {
83 immediate: true, 93 immediate: true,
84 } 94 }
85 ); 95 );
86 96
  97 + watch(
  98 + () => props.disabled,
  99 + (value) => {
  100 + unref(editoreRef)?.setReadOnly(value);
  101 + }
  102 + );
  103 +
87 const get = (): string => { 104 const get = (): string => {
88 - return unref(editoreRef)?.getText() || ''; 105 + return unref(editoreRef)?.getValue() || '';
89 }; 106 };
90 107
91 const set = (data: any) => { 108 const set = (data: any) => {
92 - return unref(editoreRef)?.set(data); 109 + return unref(editoreRef)?.setValue(getFormatValue(data));
93 }; 110 };
94 111
95 onMounted(() => { 112 onMounted(() => {
96 initialize(); 113 initialize();
97 - unref(editoreRef)?.setText(props.value || ''); 114 + unref(editoreRef)?.setValue(getFormatValue(props.value));
98 }); 115 });
99 116
100 onUnmounted(() => { 117 onUnmounted(() => {
  118 + unref(editoreRef)?.off('change', handleOnChange);
  119 + unref(editoreRef)?.off('blur', handleOnBlur);
  120 + unref(editoreRef)?.off('focus', handleOnFocus);
101 unref(editoreRef)?.destroy(); 121 unref(editoreRef)?.destroy();
  122 + unref(editoreRef)?.container.remove();
  123 + });
  124 +
  125 + const handleFormat = () => {
  126 + const value = get();
  127 + if (isString(value) && !value) return;
  128 + unref(editoreRef)?.setValue(JSON.stringify(JSON.parse(value), null, 2));
  129 + unref(editoreRef)?.clearSelection();
  130 + };
  131 +
  132 + const handleCompress = () => {
  133 + const value = get();
  134 + if (isString(value) && !value) return;
  135 + unref(editoreRef)?.setValue(JSON.stringify(JSON.parse(value)));
  136 + unref(editoreRef)?.clearSelection();
  137 + };
  138 +
  139 + const jsonEditorContainerElRef = ref<Nullable<HTMLDivElement>>();
  140 + const { isFullscreen, isSupported, toggle } = useFullscreen(jsonEditorContainerElRef);
  141 + const handleFullScreen = () => {
  142 + toggle();
  143 + };
  144 +
  145 + const getHeight = computed(() => {
  146 + const { height } = props;
  147 + return isNumber(height) ? `${height}px` : height;
102 }); 148 });
103 149
104 defineExpose({ 150 defineExpose({
@@ -108,22 +154,41 @@ @@ -108,22 +154,41 @@
108 </script> 154 </script>
109 155
110 <template> 156 <template>
111 - <div class="p-2 bg-gray-200" :style="{ height: `${height}px` }">  
112 - <div ref="jsonEditorElRef" class="jsoneditor"></div> 157 + <div
  158 + ref="jsonEditorContainerElRef"
  159 + class="p-2 bg-gray-200 flex flex-col"
  160 + :style="{ height: getHeight }"
  161 + >
  162 + <div class="w-full h-8 flex justify-between items-center">
  163 + <div>
  164 + {{ title }}
  165 + </div>
  166 + <slot name="header"></slot>
  167 + <div class="flex h-8 gap-3 justify-end items-center text-dark-500 svg:text-2xl">
  168 + <slot name="beforeFormat"></slot>
  169 + <Tooltip title="整洁">
  170 + <Icon @click="handleFormat" class="cursor-pointer" icon="gg:format-left" />
  171 + </Tooltip>
  172 + <slot name="beforeCompress"></slot>
  173 +
  174 + <Tooltip title="迷你">
  175 + <Icon @click="handleCompress" class="cursor-pointer" icon="material-symbols:compress" />
  176 + </Tooltip>
  177 + <slot name="beforeFullScreen"></slot>
  178 +
  179 + <Tooltip title="全屏">
  180 + <Icon
  181 + v-if="isSupported"
  182 + class="cursor-pointer"
  183 + :icon="
  184 + isFullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'
  185 + "
  186 + @click="handleFullScreen"
  187 + />
  188 + </Tooltip>
  189 + <slot name="afterFullScreen"></slot>
  190 + </div>
  191 + </div>
  192 + <div ref="jsonEditorElRef" class="flex-auto"></div>
113 </div> 193 </div>
114 </template> 194 </template>
115 -  
116 -<style lang="less" scoped>  
117 - .jsoneditor {  
118 - border: none !important;  
119 -  
120 - :deep(.jsoneditor) {  
121 - border: none !important;  
122 -  
123 - .ace-jsoneditor,  
124 - textarea.jsoneditor-text {  
125 - min-height: auto;  
126 - }  
127 - }  
128 - }  
129 -</style>  
@@ -13,6 +13,7 @@ export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue'; @@ -13,6 +13,7 @@ export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
13 export { default as ApiUpload } from './src/components/ApiUpload.vue'; 13 export { default as ApiUpload } from './src/components/ApiUpload.vue';
14 14
15 export { default as StructForm } from './src/externalCompns/components/StructForm/StructForm.vue'; 15 export { default as StructForm } from './src/externalCompns/components/StructForm/StructForm.vue';
  16 +export { default as JavaScriptFunctionEditor } from './src/components/JavaScriptFunctionEditor.vue';
16 17
17 //注册自定义组件 18 //注册自定义组件
18 export { 19 export {
@@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
6 <script lang="ts" setup> 6 <script lang="ts" setup>
7 import { ref, watchEffect, computed, unref, watch, reactive } from 'vue'; 7 import { ref, watchEffect, computed, unref, watch, reactive } from 'vue';
8 import { Select, Spin } from 'ant-design-vue'; 8 import { Select, Spin } from 'ant-design-vue';
9 - import { isFunction } from '/@/utils/is'; 9 + import { isFunction, isNullAndUnDef } from '/@/utils/is';
10 import { useRuleFormItem } from '/@/hooks/component/useFormItem'; 10 import { useRuleFormItem } from '/@/hooks/component/useFormItem';
11 import { useAttrs } from '/@/hooks/core/useAttrs'; 11 import { useAttrs } from '/@/hooks/core/useAttrs';
12 import { get, omit } from 'lodash-es'; 12 import { get, omit } from 'lodash-es';
@@ -30,8 +30,11 @@ @@ -30,8 +30,11 @@
30 labelField?: string; 30 labelField?: string;
31 valueField?: string; 31 valueField?: string;
32 immediate?: boolean; 32 immediate?: boolean;
33 - pagenation?: Pagination; 33 + searchField?: string;
  34 + pagination?: Pagination;
34 queryEmptyDataAgin?: boolean; 35 queryEmptyDataAgin?: boolean;
  36 + fetchSearch?: boolean;
  37 + filterOption?: (inputValue: string, options: Recordable) => boolean;
35 }>(), 38 }>(),
36 { 39 {
37 resultField: '', 40 resultField: '',
@@ -39,14 +42,15 @@ @@ -39,14 +42,15 @@
39 valueField: 'value', 42 valueField: 'value',
40 immediate: true, 43 immediate: true,
41 queryEmptyDataAgin: true, 44 queryEmptyDataAgin: true,
42 - pagenation: () => ({ page: 1, pageSize: 10 }), 45 + pagination: () => ({ page: 1, pageSize: 10 }),
  46 + fetchSearch: false,
43 } 47 }
44 ); 48 );
45 49
46 const OptionsItem = (_, { attrs }: { attrs: { vNode: any } }) => attrs.vNode; 50 const OptionsItem = (_, { attrs }: { attrs: { vNode: any } }) => attrs.vNode;
47 51
48 const options = ref<OptionsItem[]>([]); 52 const options = ref<OptionsItem[]>([]);
49 - const pagination = reactive(Object.assign({ total: 0, page: 1, pageSize: 10 }, props.pagenation)); 53 + const pagination = reactive<Record<'total' | 'page' | 'pageSize', number>>({} as any);
50 const scrollLoading = ref(false); 54 const scrollLoading = ref(false);
51 const lock = ref(false); 55 const lock = ref(false);
52 const loading = ref(false); 56 const loading = ref(false);
@@ -55,6 +59,13 @@ @@ -55,6 +59,13 @@
55 const attrs = useAttrs(); 59 const attrs = useAttrs();
56 const { t } = useI18n(); 60 const { t } = useI18n();
57 61
  62 + const getPagination = computed(() => {
  63 + return {
  64 + ...props.pagination,
  65 + ...unref(pagination),
  66 + };
  67 + });
  68 +
58 // Embedded in the form, just use the hook binding to perform form verification 69 // Embedded in the form, just use the hook binding to perform form verification
59 const [state] = useRuleFormItem(props, 'value', 'change', emitData); 70 const [state] = useRuleFormItem(props, 'value', 'change', emitData);
60 71
@@ -86,16 +97,18 @@ @@ -86,16 +97,18 @@
86 { deep: true } 97 { deep: true }
87 ); 98 );
88 99
89 - async function fetch() {  
90 - const api = props.api; 100 + async function fetch(searchText?: string) {
  101 + const { api, searchField, fetchSearch } = props;
91 if (!api || !isFunction(api)) return; 102 if (!api || !isFunction(api)) return;
  103 + const isFetchSearchFlag = fetchSearch && !isNullAndUnDef(searchText) && searchField;
92 try { 104 try {
93 !unref(getOptions).length ? (loading.value = true) : (scrollLoading.value = true); 105 !unref(getOptions).length ? (loading.value = true) : (scrollLoading.value = true);
94 lock.value = true; 106 lock.value = true;
95 const { total, items } = await api({ 107 const { total, items } = await api({
96 ...props.params, 108 ...props.params,
97 - page: pagination.page,  
98 - pageSize: pagination.pageSize, 109 + page: unref(getPagination).page,
  110 + pageSize: unref(getPagination).pageSize,
  111 + ...(isFetchSearchFlag ? { [searchField!]: searchText } : {}),
99 }); 112 });
100 113
101 pagination.total = total; 114 pagination.total = total;
@@ -105,11 +118,13 @@ @@ -105,11 +118,13 @@
105 return; 118 return;
106 } 119 }
107 if (props.resultField) { 120 if (props.resultField) {
108 - options.value = [...options.value, ...(get(items, props.resultField) || [])]; 121 + options.value = isFetchSearchFlag
  122 + ? get(items, props.resultField) || []
  123 + : [...options.value, ...(get(items, props.resultField) || [])];
109 } 124 }
110 emitChange(); 125 emitChange();
111 } catch (error) { 126 } catch (error) {
112 - pagination.page = Math.ceil(unref(getOptions).length / pagination.pageSize); 127 + pagination.page = Math.ceil(unref(getOptions).length / unref(getPagination).pageSize);
113 console.warn(error); 128 console.warn(error);
114 } finally { 129 } finally {
115 isFirstLoad.value = false; 130 isFirstLoad.value = false;
@@ -134,17 +149,39 @@ @@ -134,17 +149,39 @@
134 emitData.value = args; 149 emitData.value = args;
135 } 150 }
136 151
  152 + const sleep = async (number: number) => {
  153 + return new Promise((resolve) => {
  154 + setTimeout(() => {
  155 + resolve(number);
  156 + }, number);
  157 + });
  158 + };
  159 +
137 async function handlePopupScroll(event: MouseEvent) { 160 async function handlePopupScroll(event: MouseEvent) {
138 const { scrollHeight, scrollTop, clientHeight } = event.target as HTMLDivElement; 161 const { scrollHeight, scrollTop, clientHeight } = event.target as HTMLDivElement;
139 if (scrollTop + clientHeight >= scrollHeight) { 162 if (scrollTop + clientHeight >= scrollHeight) {
140 - if (unref(getOptions).length < pagination.total && !unref(lock)) {  
141 - pagination.page = pagination.page + 1; 163 + if (unref(getOptions).length < unref(getPagination).total && !unref(lock)) {
  164 + pagination.page = unref(getPagination).page + 1;
  165 + scrollLoading.value = true;
  166 + await sleep(500);
142 await fetch(); 167 await fetch();
143 } 168 }
144 } 169 }
145 } 170 }
146 171
147 const debounceHandlePopupScroll = useDebounceFn(handlePopupScroll, 100); 172 const debounceHandlePopupScroll = useDebounceFn(handlePopupScroll, 100);
  173 +
  174 + const handleFilterOption = async (inputValue: string, option: Recordable) => {
  175 + const { filterOption, fetchSearch } = props;
  176 + if (filterOption && isFunction(filterOption)) {
  177 + filterOption?.(inputValue, option);
  178 + return;
  179 + }
  180 +
  181 + if (fetchSearch) {
  182 + await fetch(inputValue);
  183 + }
  184 + };
148 </script> 185 </script>
149 186
150 <template> 187 <template>
@@ -153,6 +190,8 @@ @@ -153,6 +190,8 @@
153 v-bind="attrs" 190 v-bind="attrs"
154 @change="handleChange" 191 @change="handleChange"
155 :options="getOptions" 192 :options="getOptions"
  193 + :filterOption="handleFilterOption"
  194 + :showSearch="true"
156 v-model:value="state" 195 v-model:value="state"
157 @popup-scroll="debounceHandlePopupScroll" 196 @popup-scroll="debounceHandlePopupScroll"
158 > 197 >
  1 +<script lang="ts" setup>
  2 + import { Icon } from '/@/components/Icon';
  3 + import { Tooltip } from 'ant-design-vue';
  4 + import { computed, onMounted, onUnmounted, ref, shallowRef, unref, watch } from 'vue';
  5 + import AceEditor, { Ace } from 'ace-builds';
  6 + import workerJavascriptUrl from 'ace-builds/src-noconflict/worker-javascript?url';
  7 + import githubTheme from 'ace-builds/src-noconflict/theme-github?url';
  8 + import 'ace-builds/src-noconflict/mode-javascript';
  9 + import 'ace-builds/src-noconflict/ext-searchbox';
  10 + import 'ace-builds/src-noconflict/ext-language_tools';
  11 + import 'ace-builds/src-noconflict/snippets/javascript';
  12 + import { useBeautify } from '/@/hooks/business/useBeautify';
  13 + import { useFullscreen } from '@vueuse/core';
  14 + import { isNumber } from '/@/utils/is';
  15 +
  16 + const emit = defineEmits(['update:value', 'focus', 'blur']);
  17 + const props = withDefaults(
  18 + defineProps<{
  19 + functionName?: string;
  20 + paramsName?: string[];
  21 + height?: number | string;
  22 + value?: string;
  23 + disabled?: boolean;
  24 + validateStatus?: boolean;
  25 + }>(),
  26 + {
  27 + functionName: 'method',
  28 + paramsName: () => [],
  29 + height: 200,
  30 + value: '',
  31 + }
  32 + );
  33 +
  34 + const getHeight = computed(() => {
  35 + const { height } = props;
  36 + return isNumber(height) ? `${height}px` : height;
  37 + });
  38 +
  39 + const javaScriptEditorElRef = ref<Nullable<HTMLDivElement>>();
  40 + const editorInstance = shallowRef<Ace.Editor>();
  41 + const isFocus = ref(false);
  42 +
  43 + const handleFocus = () => {
  44 + isFocus.value = true;
  45 + emit('focus', unref(editorInstance));
  46 + };
  47 +
  48 + const handleBlur = () => {
  49 + isFocus.value = false;
  50 + emit('update:value', get());
  51 + emit('blur', unref(editorInstance));
  52 + };
  53 +
  54 + const handleChange = () => {
  55 + emit('update:value', get());
  56 + };
  57 +
  58 + const initEditor = () => {
  59 + AceEditor.config.setModuleUrl('ace/mode/javascript_worker', workerJavascriptUrl);
  60 + AceEditor.config.setModuleUrl('ace/theme/github', githubTheme);
  61 + const editor = AceEditor.edit(unref(javaScriptEditorElRef)!, {
  62 + mode: 'ace/mode/javascript',
  63 + });
  64 + editor.setTheme('ace/theme/github');
  65 + editor.setOptions({
  66 + fontSize: 14,
  67 + enableBasicAutocompletion: true,
  68 + enableSnippets: true,
  69 + enableLiveAutocompletion: true,
  70 + });
  71 +
  72 + editor.on('focus', handleFocus);
  73 + editor.on('blur', handleBlur);
  74 + editor.on('change', handleChange);
  75 + editorInstance.value = editor;
  76 + editor.setValue(props.value);
  77 + editor?.clearSelection();
  78 + unref(editorInstance)?.setReadOnly(props.disabled);
  79 + };
  80 +
  81 + const get = () => {
  82 + return unref(editorInstance)?.getValue();
  83 + };
  84 +
  85 + const set = (val: string, cursorPos?: number) => {
  86 + return unref(editorInstance)?.setValue(val, cursorPos);
  87 + };
  88 +
  89 + onMounted(() => {
  90 + initEditor();
  91 + });
  92 +
  93 + onUnmounted(() => {
  94 + unref(editorInstance)?.off('change', handleChange);
  95 + unref(editorInstance)?.off('focus', handleFocus);
  96 + unref(editorInstance)?.off('blur', handleBlur);
  97 + unref(editorInstance)?.destroy();
  98 + unref(editorInstance)?.container.remove();
  99 + });
  100 +
  101 + const { beautifyJs } = useBeautify();
  102 + const handleFormatCode = async () => {
  103 + const res = await beautifyJs(get() || '', { indent_size: 4, wrap_line_length: 60 });
  104 + set(res || '', -1);
  105 + };
  106 +
  107 + const jsFunctionContainerElRef = ref<Nullable<HTMLDivElement>>();
  108 + const { isFullscreen, isSupported, toggle } = useFullscreen(jsFunctionContainerElRef);
  109 + const handleFullScreen = () => {
  110 + toggle();
  111 + };
  112 +
  113 + watch(
  114 + () => props.value,
  115 + (value) => {
  116 + // const position = unref(editorInstance)?.getCursorPosition();
  117 + if (unref(isFocus)) return;
  118 + set(value);
  119 + unref(editorInstance)?.clearSelection();
  120 + // position && unref(editorInstance)?.moveCursorToPosition(position!);
  121 + },
  122 + {
  123 + immediate: true,
  124 + }
  125 + );
  126 +
  127 + watch(
  128 + () => props.disabled,
  129 + (value) => {
  130 + unref(editorInstance)?.setReadOnly(value);
  131 + }
  132 + );
  133 +
  134 + defineExpose({
  135 + get,
  136 + set,
  137 + });
  138 +</script>
  139 +
  140 +<template>
  141 + <section
  142 + ref="jsFunctionContainerElRef"
  143 + class="p-2 bg-gray-200 flex flex-col"
  144 + :style="{ height: getHeight }"
  145 + >
  146 + <head class="flex justify-between h-8 items-center">
  147 + <div class="font-bold">
  148 + <span>function</span>
  149 + <span class="ml-1">{{ functionName }}</span>
  150 + <span>({{ paramsName.join(',') }})</span>
  151 + <span class="ml-1">{</span>
  152 + </div>
  153 + <div class="flex gap-3 items-center svg:text-2xl">
  154 + <slot name="beforeFormat"></slot>
  155 +
  156 + <Tooltip title="整洁">
  157 + <Icon class="cursor-pointer" icon="gg:format-left" @click="handleFormatCode" />
  158 + </Tooltip>
  159 + <slot name="beforeFullScreen"></slot>
  160 + <Tooltip title="全屏">
  161 + <Icon
  162 + v-if="isSupported"
  163 + class="cursor-pointer"
  164 + :icon="
  165 + isFullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'
  166 + "
  167 + @click="handleFullScreen"
  168 + />
  169 + </Tooltip>
  170 + <slot name="afterFullScreen"></slot>
  171 + </div>
  172 + </head>
  173 + <main ref="javaScriptEditorElRef" class="flex-auto"> </main>
  174 + <footer class="font-bold">}</footer>
  175 + </section>
  176 +</template>
@@ -130,4 +130,6 @@ export type ComponentType = @@ -130,4 +130,6 @@ export type ComponentType =
130 | 'ControlGroup' 130 | 'ControlGroup'
131 | 'JSONEditor' 131 | 'JSONEditor'
132 | 'OrgTreeSelect' 132 | 'OrgTreeSelect'
133 - | 'ExtendDesc'; 133 + | 'ExtendDesc'
  134 + | 'JavaScriptFunctionEditor'
  135 + | 'JavaScriptFilterFunctionModal';
@@ -19,4 +19,8 @@ export enum DictEnum { @@ -19,4 +19,8 @@ export enum DictEnum {
19 DISABLE_CUSTOMER_AUTH = 'disabled_customer_auth', 19 DISABLE_CUSTOMER_AUTH = 'disabled_customer_auth',
20 // 寄存器数据格式 20 // 寄存器数据格式
21 REGISTER_DATA_FORMAT = 'register_data_format', 21 REGISTER_DATA_FORMAT = 'register_data_format',
  22 + // 消息类型 规则节点 Filter message type switch
  23 + MESSAGE_TYPES_FILTER = 'message_types_filter',
  24 + // 实体类型 规则节点 Filter originator types switch
  25 + ORIGINATOR_TYPES = 'originator_types',
22 } 26 }
  1 +import { saveOrEditDictItem } from '/@/api/system/dict';
  2 +import { SysDictItem } from '/@/api/system/model/dictModel';
  3 +
  4 +interface BatchSettingParamsType {
  5 + dictId: string;
  6 + list: any[];
  7 + transformText: (item: any) => any;
  8 + transformValue: (item: any) => any;
  9 + update?: boolean;
  10 +}
  11 +
  12 +export function useBatchSettingDict() {
  13 + const batchSetting = async (options: BatchSettingParamsType) => {
  14 + const { dictId, list, transformText, transformValue, update = false } = options;
  15 + if (!transformText || !transformValue) return;
  16 + let index = 1;
  17 + for (const item of list) {
  18 + const params: Partial<SysDictItem> & { dictId: string } = {
  19 + dictId,
  20 + sort: index++,
  21 + status: 1,
  22 + itemText: transformText(item),
  23 + itemValue: transformValue(item),
  24 + };
  25 +
  26 + await saveOrEditDictItem(params as SysDictItem, update);
  27 + }
  28 + };
  29 +
  30 + const batchDelete = () => {};
  31 +
  32 + return {
  33 + batchSetting,
  34 + batchDelete,
  35 + };
  36 +}
  1 +export function useBeautify() {
  2 + let jsBeautifyModule: any;
  3 +
  4 + async function loadJsBeautify() {
  5 + if (jsBeautifyModule) return jsBeautifyModule;
  6 + jsBeautifyModule = await import('js-beautify/js/lib/beautifier');
  7 + return jsBeautifyModule;
  8 + }
  9 +
  10 + async function beautifyJs(source: string, options?: any) {
  11 + const module = await loadJsBeautify();
  12 + return module.js(source, options);
  13 + }
  14 + return { beautifyJs };
  15 +}
  1 +import { isObject, isString } from '/@/utils/is';
  2 +
  3 +export function useJsonParse(value: any, defaultValue = {}) {
  4 + let flag = false;
  5 + try {
  6 + value = JSON.parse(value);
  7 + if (isObject(value) || (isString(value) && value.trim() === '')) flag = true;
  8 + } catch (error) {
  9 + value = defaultValue;
  10 + }
  11 + return { flag, value };
  12 +}
@@ -91,7 +91,7 @@ export const formSchema: FormSchema[] = [ @@ -91,7 +91,7 @@ export const formSchema: FormSchema[] = [
91 valueField: 'fileList', 91 valueField: 'fileList',
92 componentProps: () => { 92 componentProps: () => {
93 return { 93 return {
94 - listType: 'picture-card', 94 + // listType: 'picture-card',
95 maxFileLimit: 1, 95 maxFileLimit: 1,
96 api: async (file: File) => { 96 api: async (file: File) => {
97 try { 97 try {
@@ -110,6 +110,10 @@ export const formSchema: FormSchema[] = [ @@ -110,6 +110,10 @@ export const formSchema: FormSchema[] = [
110 onPreview: (fileList: FileItem) => { 110 onPreview: (fileList: FileItem) => {
111 createImgPreview({ imageList: [fileList.url!] }); 111 createImgPreview({ imageList: [fileList.url!] });
112 }, 112 },
  113 + showUploadList: {
  114 + showDownloadIcon: true,
  115 + showRemoveIcon: true,
  116 + },
113 }; 117 };
114 }, 118 },
115 }, 119 },
@@ -46,7 +46,7 @@ export const formSchema: FormSchema[] = [ @@ -46,7 +46,7 @@ export const formSchema: FormSchema[] = [
46 valueField: 'fileList', 46 valueField: 'fileList',
47 componentProps: () => { 47 componentProps: () => {
48 return { 48 return {
49 - listType: 'picture-card', 49 + // listType: 'picture-card',
50 maxFileLimit: 1, 50 maxFileLimit: 1,
51 api: async (file: File) => { 51 api: async (file: File) => {
52 try { 52 try {
@@ -62,6 +62,10 @@ export const formSchema: FormSchema[] = [ @@ -62,6 +62,10 @@ export const formSchema: FormSchema[] = [
62 return {}; 62 return {};
63 } 63 }
64 }, 64 },
  65 + showUploadList: true,
  66 + onDownload(file) {
  67 + console.log(file);
  68 + },
65 onPreview: (fileList: FileItem) => { 69 onPreview: (fileList: FileItem) => {
66 createImgPreview({ imageList: [fileList.url!] }); 70 createImgPreview({ imageList: [fileList.url!] });
67 }, 71 },
@@ -23,7 +23,7 @@ @@ -23,7 +23,7 @@
23 layout: 'vertical', 23 layout: 'vertical',
24 }); 24 });
25 25
26 - const { genForm } = useGenDynamicForm(); 26 + const { genForm, transformValue } = useGenDynamicForm();
27 27
28 const keys = ref<string[]>([]); 28 const keys = ref<string[]>([]);
29 29
@@ -127,8 +127,8 @@ @@ -127,8 +127,8 @@
127 127
128 sendValue.value = await genModbusCommand(unref(modBUSForm)); 128 sendValue.value = await genModbusCommand(unref(modBUSForm));
129 } else { 129 } else {
130 - const _value = getFieldsValue();  
131 - 130 + await validate();
  131 + const _value = transformValue(getFieldsValue());
132 sendValue.value = unref(keys).reduce((prev, next) => { 132 sendValue.value = unref(keys).reduce((prev, next) => {
133 return { ...prev, [next]: _value[next] }; 133 return { ...prev, [next]: _value[next] };
134 }, {}); 134 }, {});
1 import { DataType, Specs, StructJSON } from '/@/api/device/model/modelOfMatterModel'; 1 import { DataType, Specs, StructJSON } from '/@/api/device/model/modelOfMatterModel';
2 import { JSONEditor } from '/@/components/CodeEditor'; 2 import { JSONEditor } from '/@/components/CodeEditor';
3 import { FormSchema, useComponentRegister } from '/@/components/Form'; 3 import { FormSchema, useComponentRegister } from '/@/components/Form';
  4 +import { useJsonParse } from '/@/hooks/business/useJsonParse';
4 import { DataTypeEnum } from '/@/views/device/profiles/step/cpns/physical/cpns/config'; 5 import { DataTypeEnum } from '/@/views/device/profiles/step/cpns/physical/cpns/config';
5 6
6 export interface BasicCreateFormParams { 7 export interface BasicCreateFormParams {
@@ -20,7 +21,6 @@ export const useGenDynamicForm = () => { @@ -20,7 +21,6 @@ export const useGenDynamicForm = () => {
20 const { specs, type } = dataType; 21 const { specs, type } = dataType;
21 const { valueRange, step } = specs! as Partial<Specs>; 22 const { valueRange, step } = specs! as Partial<Specs>;
22 const { max, min } = valueRange || {}; 23 const { max, min } = valueRange || {};
23 - console.log({ max, min });  
24 return { 24 return {
25 field: identifier, 25 field: identifier,
26 label: functionName, 26 label: functionName,
@@ -107,6 +107,19 @@ export const useGenDynamicForm = () => { @@ -107,6 +107,19 @@ export const useGenDynamicForm = () => {
107 field: identifier, 107 field: identifier,
108 label: functionName, 108 label: functionName,
109 component: 'JSONEditor', 109 component: 'JSONEditor',
  110 + valueField: 'value',
  111 + changeEvent: 'update:value',
  112 + rules: [
  113 + {
  114 + validator: (_rule, value: any) => {
  115 + if (value) {
  116 + const { flag } = useJsonParse(value);
  117 + if (!flag) return Promise.reject(`${functionName} 不是一个有效的JSON对象`);
  118 + }
  119 + return Promise.resolve();
  120 + },
  121 + },
  122 + ],
110 }; 123 };
111 }; 124 };
112 125
@@ -118,11 +131,14 @@ export const useGenDynamicForm = () => { @@ -118,11 +131,14 @@ export const useGenDynamicForm = () => {
118 [DataTypeEnum.IS_STRUCT]: createInputJson, 131 [DataTypeEnum.IS_STRUCT]: createInputJson,
119 }; 132 };
120 133
  134 + const fieldTypeMap = new Map<string, DataTypeEnum>();
121 const genForm = (schemas: StructJSON[]) => { 135 const genForm = (schemas: StructJSON[]) => {
  136 + fieldTypeMap.clear();
122 const formSchema = schemas.map((item) => { 137 const formSchema = schemas.map((item) => {
123 const { functionName, identifier, dataType } = item; 138 const { functionName, identifier, dataType } = item;
124 const { type } = dataType || {}; 139 const { type } = dataType || {};
125 140
  141 + fieldTypeMap.set(identifier!, dataType!.type);
126 const method = schemaMethod[type!]; 142 const method = schemaMethod[type!];
127 143
128 const formSchema = method?.({ 144 const formSchema = method?.({
@@ -137,5 +153,21 @@ export const useGenDynamicForm = () => { @@ -137,5 +153,21 @@ export const useGenDynamicForm = () => {
137 return formSchema.filter(Boolean); 153 return formSchema.filter(Boolean);
138 }; 154 };
139 155
140 - return { genForm }; 156 + const transformValue = (value: Recordable) => {
  157 + return Object.keys(value || {}).reduce((prev, next) => {
  158 + const dataType = fieldTypeMap.get(next)!;
  159 +
  160 + let itemValue = value[next];
  161 + if (dataType === DataTypeEnum.IS_STRUCT) {
  162 + const { value } = useJsonParse(itemValue);
  163 + itemValue = value;
  164 + }
  165 + return {
  166 + ...prev,
  167 + [next]: itemValue,
  168 + };
  169 + }, {} as Recordable);
  170 + };
  171 +
  172 + return { genForm, transformValue };
141 }; 173 };
  1 +/**
  2 + * @description 方向
  3 + */
  4 +export enum DirectionEnum {
  5 + FROM = 'FROM',
  6 + TO = 'TO',
  7 +}
  8 +
  9 +export enum DirectionNameEnum {
  10 + FROM = '从',
  11 + TO = '到',
  12 +}
  13 +/**
  14 + * @description 关联类型
  15 + */
  16 +export enum RelationTypeEnum {
  17 + CONTAINS = 'contains',
  18 + MANAGES = 'manages',
  19 +}
  20 +
  21 +/**
  22 + * @description 类型
  23 + */
  24 +export enum EntityTypeEnum {
  25 + DEVICE = 'DEVICE',
  26 + ASSET = 'ASSET',
  27 + ENTITY_VIEW = 'ENTITY_VIEW',
  28 + TENANT = 'TENANT',
  29 + CUSTOMER = 'CUSTOMER',
  30 + USER = 'USER',
  31 + DASHBOARD = 'DASHBOARD',
  32 + EDGE = 'EDGE',
  33 +}
  34 +
  35 +export enum EntityTypeNameEnum {
  36 + DEVICE = '设备',
  37 + ASSET = '资产',
  38 + ENTITY_VIEW = '实体视图',
  39 + TENANT = '租户',
  40 + CUSTOMER = '客户',
  41 + USER = '用户',
  42 + DASHBOARD = '仪表板',
  43 + EDGE = 'Edge',
  44 +}
  45 +
  46 +/**
  47 + * @description Filter Perimeter type
  48 + */
  49 +export enum PerimeterTypeEnum {
  50 + POLYGON = 'POLYGON',
  51 + CIRCLE = 'CIRCLE',
  52 +}
  53 +
  54 +/**
  55 + * @description Filter Range util
  56 + */
  57 +export enum RangeUtilEnum {
  58 + METER = 'METER',
  59 + KILOMETER = 'KILOMETER',
  60 + FOOT = 'FOOT',
  61 + MILE = 'MILE',
  62 + NAUTICAL_MILE = 'NAUTICAL_MILE',
  63 +}
  64 +
  65 +export enum RangeUtilNameEnum {
  66 + METER = 'Meter',
  67 + KILOMETER = 'Kilometer',
  68 + FOOT = 'Foot',
  69 + MILE = 'Mile',
  70 + NAUTICAL_MILE = 'Nautical mile',
  71 +}
  72 +
  73 +export enum MessageTypesFilterEnum {
  74 + POST_ATTRIBUTES = 'POST_ATTRIBUTES',
  75 + POST_TELEMETRY = 'POST_TELEMETRY',
  76 + RPC_REQUEST_FROM_DEVICE = 'RPC_REQUEST_FROM_DEVICE',
  77 + RPC_REQUEST_TO_DEVICE = 'RPC_REQUEST_TO_DEVICE',
  78 + ACTIVITY_EVENT = 'ACTIVITY_EVENT',
  79 + INACTIVITY_EVENT = 'INACTIVITY_EVENT',
  80 + CONNECT_EVENT = 'CONNECT_EVENT',
  81 + DISCONNECT_EVENT = 'DISCONNECT_EVENT',
  82 + ENTITY_CREATED = 'ENTITY_CREATED',
  83 + ENTITY_UPDATED = 'ENTITY_UPDATED',
  84 + ENTITY_DELETED = 'ENTITY_DELETED',
  85 + ENTITY_ASSIGNED = 'ENTITY_ASSIGNED',
  86 + ENTITY_UNASSIGNED = 'ENTITY_UNASSIGNED',
  87 + ATTRIBUTES_UPDATED = 'ATTRIBUTES_UPDATED',
  88 + ATTRIBUTES_DELETED = 'ATTRIBUTES_DELETED',
  89 + TIMESERIES_UPDATED = 'TIMESERIES_UPDATED',
  90 + TIMESERIES_DELETED = 'TIMESERIES_DELETED',
  91 + RPC_QUEUED = 'RPC_QUEUED',
  92 + RPC_DELIVERED = 'RPC_DELIVERED',
  93 + RPC_SUCCESSFUL = 'RPC_SUCCESSFUL',
  94 + RPC_TIMEOUT = 'RPC_TIMEOUT',
  95 + RPC_FAILED = 'RPC_FAILED',
  96 +}
  97 +
  98 +export enum MessageTypesFilterNameEnum {
  99 + POST_ATTRIBUTES = 'Post attributes',
  100 + POST_TELEMETRY = 'Post telemetry',
  101 + RPC_REQUEST_FROM_DEVICE = 'RPC Request from Device',
  102 + RPC_REQUEST_TO_DEVICE = 'RPC Request to Device',
  103 + ACTIVITY_EVENT = 'Activity Event',
  104 + INACTIVITY_EVENT = 'Inactivity Event',
  105 + CONNECT_EVENT = 'Connect Event',
  106 + DISCONNECT_EVENT = 'Disconnect Event',
  107 + ENTITY_CREATED = 'Entity Created',
  108 + ENTITY_UPDATED = 'Entity Updated',
  109 + ENTITY_DELETED = 'Entity Deleted',
  110 + ENTITY_ASSIGNED = 'Entity Assigned',
  111 + ENTITY_UNASSIGNED = 'Entity Unassigned',
  112 + ATTRIBUTES_UPDATED = 'Attributes Updated',
  113 + ATTRIBUTES_DELETED = 'Attributes Deleted',
  114 + TIMESERIES_UPDATED = 'Timeseries Updated',
  115 + TIMESERIES_DELETED = 'Timeseries Deleted',
  116 + RPC_QUEUED = 'RPC Queued',
  117 + RPC_DELIVERED = 'RPC Delivered',
  118 + RPC_SUCCESSFUL = 'RPC Successful',
  119 + RPC_TIMEOUT = 'RPC Timeout',
  120 + RPC_FAILED = 'RPC Failed',
  121 +}
@@ -5,16 +5,85 @@ export enum FetchNodeComFlagTypeENum { @@ -5,16 +5,85 @@ export enum FetchNodeComFlagTypeENum {
5 5
6 export enum NodeBindDataFieldEnum { 6 export enum NodeBindDataFieldEnum {
7 NAME = 'name', 7 NAME = 'name',
8 - ALARM_STATUS_LIST = 'alarmStatusList',  
9 DESCRIPTION = 'description', 8 DESCRIPTION = 'description',
10 DEBUG_MODE = 'debugMode', 9 DEBUG_MODE = 'debugMode',
  10 +
  11 + // Filter Check Alarm status
  12 + ALARM_STATUS_LIST = 'alarmStatusList',
  13 +
  14 + // Filter Check Existence Fields
  15 + MESSAGE_NAMES = 'messageNames',
  16 + METADATA_NAMES = 'metadataNames',
  17 + CHECK_ALL_KEYS = 'checkAllKeys',
  18 +
  19 + // Filter Check Relation
  20 + DIRECTION = 'direction',
  21 + CHECK_FOR_SINGLE_ENTITY = 'checkForSingleEntity',
  22 + ENTITY_TYPE = 'entityType',
  23 + RELEATION_TYPE = 'relationType',
  24 + ENTITY_ID = 'entityId',
  25 +
  26 + // Filter Gps geofencing filter
  27 + LATITUDE_KEY_NAME = 'latitudeKeyName',
  28 + LONGITUDE_KEY_NAME = 'longitudeKeyName',
  29 + PERIMETER_TYPE = 'perimeterType',
  30 + FETCH_PERIMETER_INFO_FROM_MESSAGE_METADATA = 'fetchPerimeterInfoFromMessageMetadata',
  31 + PERIMETER_KEY_NAME = 'perimeterKeyName',
  32 + CENTER_LATITUDE = 'centerLatitude',
  33 + CENTER_LONGITUDE = 'centerLongitude',
  34 + RANGE = 'range',
  35 + RANGE_UNIT = 'rangeUnit',
  36 + POLYGONS_DEFINITION = 'polygonsDefinition',
  37 +
  38 + // Filter Message Type
  39 + MESSAGE_TYPES = 'messageTypes',
  40 +
  41 + // Filter Originator Type
  42 + ORIGINATOR_TYPES = 'originatorTypes',
  43 +
  44 + // Filter Script
  45 + JS_SCRIPT = 'jsScript',
11 } 46 }
12 47
13 export enum NodeBindDataFieldNameEnum { 48 export enum NodeBindDataFieldNameEnum {
14 NAME = '名称', 49 NAME = '名称',
15 - ALARM_STATUS_LIST = 'Alarm status filter',  
16 DESCRIPTION = '说明', 50 DESCRIPTION = '说明',
17 DEBUG_MODE = '调试模式', 51 DEBUG_MODE = '调试模式',
  52 +
  53 + // Filter Check Alarm status
  54 + ALARM_STATUS_LIST = 'Alarm status filter',
  55 +
  56 + // Filter Check Existence Fields
  57 + MESSAGE_NAMES = '消息数据',
  58 + METADATA_NAMES = '消息元数据',
  59 + CHECK_ALL_KEYS = '检查所有选择的键是否都存在',
  60 +
  61 + // Filter Check Relation
  62 + DIRECTION = '方向',
  63 + CHECK_FOR_SINGLE_ENTITY = 'Check relation to specific entity',
  64 + ENTITY_TYPE = '类型',
  65 + RELEATION_TYPE = '关联类型',
  66 +
  67 + // Filter Gps geofencing filter
  68 + LATITUDE_KEY_NAME = 'Latitude Key Name',
  69 + LONGITUDE_KEY_NAME = 'Longitude Key Name',
  70 + PERIMETER_TYPE = 'Perimeter Type',
  71 + FETCH_PERIMETER_INFO_FROM_MESSAGE_METADATA = 'Fetch perimeter information from message metadata',
  72 + CENTER_LATITUDE = 'Center latitude',
  73 + CENTER_LONGITUDE = 'Center longitude',
  74 + RANGE = 'Range',
  75 + RANGE_UNIT = 'Range unit',
  76 + PERIMETER_KEY_NAME = 'Perimeter key name',
  77 + POLYGONS_DEFINITION = 'Polygons definition',
  78 +
  79 + // Filter Message Type
  80 + MESSAGE_TYPES = 'Message Types Filter',
  81 +
  82 + // Filter Originator Type
  83 + ORIGINATOR_TYPES = 'Originator types filter',
  84 +
  85 + // Filter Script
  86 + JS_SCRIPT = 'Filter',
18 } 87 }
19 88
20 export enum EdgeBindDataFieldEnum { 89 export enum EdgeBindDataFieldEnum {
@@ -53,7 +53,7 @@ export function useBasicDataTransform() { @@ -53,7 +53,7 @@ export function useBasicDataTransform() {
53 53
54 nodes.forEach((item, index) => indexMap.set(index, item)); 54 nodes.forEach((item, index) => indexMap.set(index, item));
55 55
56 - connections.forEach((item) => { 56 + (connections || []).forEach((item) => {
57 const { fromIndex, toIndex, type } = item; 57 const { fromIndex, toIndex, type } = item;
58 const key = `${fromIndex}${SEPARATOR}${toIndex}`; 58 const key = `${fromIndex}${SEPARATOR}${toIndex}`;
59 if (!groupByConnections.has(key)) groupByConnections.set(key, []); 59 if (!groupByConnections.has(key)) groupByConnections.set(key, []);
@@ -124,7 +124,7 @@ export function useSaveAndRedo() { @@ -124,7 +124,7 @@ export function useSaveAndRedo() {
124 firstNodeIndex, 124 firstNodeIndex,
125 ruleChainId: { 125 ruleChainId: {
126 entityType: 'RULE_CHAIN', 126 entityType: 'RULE_CHAIN',
127 - id: 'd6ddea70-25da-11ee-bc6b-47e715464e68', 127 + id: '992ddda0-3d97-11ee-8b46-0dfb0900ab17',
128 }, 128 },
129 }); 129 });
130 130
@@ -140,7 +140,7 @@ export function useSaveAndRedo() { @@ -140,7 +140,7 @@ export function useSaveAndRedo() {
140 try { 140 try {
141 loading.value = true; 141 loading.value = true;
142 142
143 - const id = 'd6ddea70-25da-11ee-bc6b-47e715464e68'; 143 + const id = '992ddda0-3d97-11ee-8b46-0dfb0900ab17';
144 144
145 const data = await getRuleChainData(id); 145 const data = await getRuleChainData(id);
146 146
@@ -3,8 +3,27 @@ import { FormSchema } from '/@/components/Form'; @@ -3,8 +3,27 @@ import { FormSchema } from '/@/components/Form';
3 3
4 export const formSchemas: FormSchema[] = [ 4 export const formSchemas: FormSchema[] = [
5 { 5 {
6 - field: NodeBindDataFieldEnum.NAME,  
7 - component: 'Input',  
8 - label: NodeBindDataFieldNameEnum.NAME, 6 + field: NodeBindDataFieldEnum.MESSAGE_NAMES,
  7 + component: 'Select',
  8 + label: NodeBindDataFieldNameEnum.MESSAGE_NAMES,
  9 + componentProps: {
  10 + mode: 'tags',
  11 + getPopupContainer: () => document.body,
  12 + },
  13 + },
  14 + {
  15 + field: NodeBindDataFieldEnum.METADATA_NAMES,
  16 + component: 'Select',
  17 + label: NodeBindDataFieldNameEnum.METADATA_NAMES,
  18 + componentProps: {
  19 + mode: 'tags',
  20 + getPopupContainer: () => document.body,
  21 + },
  22 + },
  23 + {
  24 + field: NodeBindDataFieldEnum.CHECK_ALL_KEYS,
  25 + component: 'Checkbox',
  26 + label: NodeBindDataFieldNameEnum.CHECK_ALL_KEYS,
  27 + labelWidth: 200,
9 }, 28 },
10 ]; 29 ];
  1 +import {
  2 + DirectionEnum,
  3 + DirectionNameEnum,
  4 + EntityTypeEnum,
  5 + EntityTypeNameEnum,
  6 + RelationTypeEnum,
  7 +} from '../../../enum/form';
1 import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node'; 8 import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node';
  9 +import { deviceProfilePage } from '/@/api/device/deviceManager';
2 import { FormSchema } from '/@/components/Form'; 10 import { FormSchema } from '/@/components/Form';
3 11
4 export const formSchemas: FormSchema[] = [ 12 export const formSchemas: FormSchema[] = [
@@ -7,4 +15,64 @@ export const formSchemas: FormSchema[] = [ @@ -7,4 +15,64 @@ export const formSchemas: FormSchema[] = [
7 component: 'Input', 15 component: 'Input',
8 label: NodeBindDataFieldNameEnum.NAME, 16 label: NodeBindDataFieldNameEnum.NAME,
9 }, 17 },
  18 + {
  19 + field: NodeBindDataFieldEnum.CHECK_FOR_SINGLE_ENTITY,
  20 + component: 'Checkbox',
  21 + label: NodeBindDataFieldNameEnum.CHECK_FOR_SINGLE_ENTITY,
  22 + labelWidth: 220,
  23 + },
  24 + {
  25 + field: NodeBindDataFieldEnum.DIRECTION,
  26 + component: 'Select',
  27 + label: NodeBindDataFieldEnum.DIRECTION,
  28 + componentProps: {
  29 + options: [
  30 + { label: DirectionNameEnum.FROM, value: DirectionEnum.FROM },
  31 + { label: DirectionNameEnum.TO, value: DirectionEnum.TO },
  32 + ],
  33 + },
  34 + },
  35 + {
  36 + field: NodeBindDataFieldEnum.ENTITY_TYPE,
  37 + component: 'Select',
  38 + label: NodeBindDataFieldNameEnum.ENTITY_TYPE,
  39 + colProps: { span: 8 },
  40 + componentProps: {
  41 + options: Object.keys(EntityTypeEnum).map((key) => ({
  42 + label: EntityTypeNameEnum[key],
  43 + value: key,
  44 + })),
  45 + },
  46 + },
  47 + {
  48 + field: NodeBindDataFieldEnum.ENTITY_ID,
  49 + component: 'ApiSelectScrollLoad',
  50 + label: ' ',
  51 + colProps: { span: 16 },
  52 + show: ({ model }) => model[NodeBindDataFieldEnum.ENTITY_TYPE],
  53 + componentProps: ({ formModel }) => {
  54 + const entityType = formModel[NodeBindDataFieldEnum.ENTITY_TYPE];
  55 +
  56 + return {
  57 + showSearch: true,
  58 + api: deviceProfilePage,
  59 + resultField: 'items',
  60 + labelField: 'name',
  61 + valueField: 'id',
  62 + placeholder: `请选择${EntityTypeNameEnum[entityType]}`,
  63 + };
  64 + },
  65 + },
  66 + {
  67 + field: NodeBindDataFieldEnum.RELEATION_TYPE,
  68 + component: 'Select',
  69 + label: NodeBindDataFieldNameEnum.RELEATION_TYPE,
  70 + defaultValue: RelationTypeEnum.CONTAINS,
  71 + componentProps: {
  72 + options: [
  73 + { label: RelationTypeEnum.CONTAINS, value: RelationTypeEnum.CONTAINS },
  74 + { label: RelationTypeEnum.MANAGES, value: RelationTypeEnum.MANAGES },
  75 + ],
  76 + },
  77 + },
10 ]; 78 ];
  1 +import { PerimeterTypeEnum, RangeUtilEnum, RangeUtilNameEnum } from '../../../enum/form';
1 import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node'; 2 import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node';
2 import { FormSchema } from '/@/components/Form'; 3 import { FormSchema } from '/@/components/Form';
3 4
4 export const formSchemas: FormSchema[] = [ 5 export const formSchemas: FormSchema[] = [
5 { 6 {
6 - field: NodeBindDataFieldEnum.NAME, 7 + field: NodeBindDataFieldEnum.LATITUDE_KEY_NAME,
7 component: 'Input', 8 component: 'Input',
8 - label: NodeBindDataFieldNameEnum.NAME, 9 + label: NodeBindDataFieldNameEnum.LATITUDE_KEY_NAME,
  10 + },
  11 + {
  12 + field: NodeBindDataFieldEnum.LONGITUDE_KEY_NAME,
  13 + component: 'Input',
  14 + label: NodeBindDataFieldNameEnum.LONGITUDE_KEY_NAME,
  15 + },
  16 + {
  17 + field: NodeBindDataFieldEnum.PERIMETER_TYPE,
  18 + component: 'Select',
  19 + label: NodeBindDataFieldNameEnum.PERIMETER_TYPE,
  20 + componentProps: {
  21 + options: Object.keys(PerimeterTypeEnum).map((value) => ({ label: value, value })),
  22 + },
  23 + },
  24 + {
  25 + field: NodeBindDataFieldEnum.FETCH_PERIMETER_INFO_FROM_MESSAGE_METADATA,
  26 + component: 'Checkbox',
  27 + label: NodeBindDataFieldNameEnum.FETCH_PERIMETER_INFO_FROM_MESSAGE_METADATA,
  28 + labelWidth: 350,
  29 + },
  30 + {
  31 + field: NodeBindDataFieldEnum.PERIMETER_KEY_NAME,
  32 + component: 'Input',
  33 + label: NodeBindDataFieldNameEnum.PERIMETER_KEY_NAME,
  34 + show: ({ model }) => model[NodeBindDataFieldEnum.FETCH_PERIMETER_INFO_FROM_MESSAGE_METADATA],
  35 + },
  36 + {
  37 + field: NodeBindDataFieldEnum.POLYGONS_DEFINITION,
  38 + component: 'Input',
  39 + label: NodeBindDataFieldNameEnum.POLYGONS_DEFINITION,
  40 + helpMessage:
  41 + 'Please, use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].',
  42 + show: ({ model }) =>
  43 + !model[NodeBindDataFieldEnum.FETCH_PERIMETER_INFO_FROM_MESSAGE_METADATA] &&
  44 + model[NodeBindDataFieldEnum.PERIMETER_TYPE] === PerimeterTypeEnum.POLYGON,
  45 + },
  46 + {
  47 + field: NodeBindDataFieldEnum.CENTER_LATITUDE,
  48 + component: 'InputNumber',
  49 + label: NodeBindDataFieldNameEnum.CENTER_LATITUDE,
  50 + colProps: { span: 12 },
  51 + show: ({ model }) =>
  52 + model[NodeBindDataFieldEnum.PERIMETER_TYPE] === PerimeterTypeEnum.CIRCLE &&
  53 + !model[NodeBindDataFieldEnum.FETCH_PERIMETER_INFO_FROM_MESSAGE_METADATA],
  54 + },
  55 + {
  56 + field: NodeBindDataFieldEnum.CENTER_LONGITUDE,
  57 + component: 'InputNumber',
  58 + label: NodeBindDataFieldNameEnum.CENTER_LONGITUDE,
  59 + colProps: { span: 12 },
  60 + show: ({ model }) =>
  61 + model[NodeBindDataFieldEnum.PERIMETER_TYPE] === PerimeterTypeEnum.CIRCLE &&
  62 + !model[NodeBindDataFieldEnum.FETCH_PERIMETER_INFO_FROM_MESSAGE_METADATA],
  63 + },
  64 + {
  65 + field: NodeBindDataFieldEnum.RANGE,
  66 + component: 'InputNumber',
  67 + label: NodeBindDataFieldNameEnum.RANGE,
  68 + colProps: { span: 12 },
  69 + show: ({ model }) =>
  70 + model[NodeBindDataFieldEnum.PERIMETER_TYPE] === PerimeterTypeEnum.CIRCLE &&
  71 + !model[NodeBindDataFieldEnum.FETCH_PERIMETER_INFO_FROM_MESSAGE_METADATA],
  72 + },
  73 + {
  74 + field: NodeBindDataFieldEnum.RANGE_UNIT,
  75 + component: 'Select',
  76 + label: NodeBindDataFieldNameEnum.RANGE_UNIT,
  77 + colProps: { span: 12 },
  78 + show: ({ model }) =>
  79 + model[NodeBindDataFieldEnum.PERIMETER_TYPE] === PerimeterTypeEnum.CIRCLE &&
  80 + !model[NodeBindDataFieldEnum.FETCH_PERIMETER_INFO_FROM_MESSAGE_METADATA],
  81 + componentProps: {
  82 + options: Object.keys(RangeUtilEnum).map((value) => ({
  83 + label: RangeUtilNameEnum[value],
  84 + value,
  85 + })),
  86 + },
9 }, 87 },
10 ]; 88 ];
1 import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node'; 1 import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node';
  2 +import { findDictItemByCode } from '/@/api/system/dict';
2 import { FormSchema } from '/@/components/Form'; 3 import { FormSchema } from '/@/components/Form';
  4 +import { DictEnum } from '/@/enums/dictEnum';
3 5
4 export const formSchemas: FormSchema[] = [ 6 export const formSchemas: FormSchema[] = [
5 { 7 {
6 - field: NodeBindDataFieldEnum.NAME,  
7 - component: 'Input',  
8 - label: NodeBindDataFieldNameEnum.NAME, 8 + field: NodeBindDataFieldEnum.MESSAGE_TYPES,
  9 + component: 'ApiSelect',
  10 + label: NodeBindDataFieldNameEnum.MESSAGE_TYPES,
  11 + componentProps: {
  12 + api: findDictItemByCode,
  13 + params: {
  14 + dictCode: DictEnum.MESSAGE_TYPES_FILTER,
  15 + },
  16 + mode: 'multiple',
  17 + labelField: 'itemText',
  18 + valueField: 'itemValue',
  19 + getPopupContainer: () => document.body,
  20 + },
9 }, 21 },
10 ]; 22 ];
1 -import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node';  
2 import { FormSchema } from '/@/components/Form'; 1 import { FormSchema } from '/@/components/Form';
3 2
4 -export const formSchemas: FormSchema[] = [  
5 - {  
6 - field: NodeBindDataFieldEnum.NAME,  
7 - component: 'Input',  
8 - label: NodeBindDataFieldNameEnum.NAME,  
9 - },  
10 -]; 3 +export const formSchemas: FormSchema[] = [];
1 import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node'; 1 import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node';
  2 +import { findDictItemByCode } from '/@/api/system/dict';
2 import { FormSchema } from '/@/components/Form'; 3 import { FormSchema } from '/@/components/Form';
  4 +import { DictEnum } from '/@/enums/dictEnum';
3 5
4 export const formSchemas: FormSchema[] = [ 6 export const formSchemas: FormSchema[] = [
5 { 7 {
6 - field: NodeBindDataFieldEnum.NAME,  
7 - component: 'Input',  
8 - label: NodeBindDataFieldNameEnum.NAME, 8 + field: NodeBindDataFieldEnum.ORIGINATOR_TYPES,
  9 + component: 'ApiSelect',
  10 + label: NodeBindDataFieldNameEnum.ORIGINATOR_TYPES,
  11 + componentProps: {
  12 + api: findDictItemByCode,
  13 + params: {
  14 + dictCode: DictEnum.ORIGINATOR_TYPES,
  15 + },
  16 + mode: 'multiple',
  17 + labelField: 'itemText',
  18 + valueField: 'itemValue',
  19 + getPopupContainer: () => document.body,
  20 + },
9 }, 21 },
10 ]; 22 ];
1 -import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node';  
2 import { FormSchema } from '/@/components/Form'; 1 import { FormSchema } from '/@/components/Form';
3 2
4 -export const formSchemas: FormSchema[] = [  
5 - {  
6 - field: NodeBindDataFieldEnum.NAME,  
7 - component: 'Input',  
8 - label: NodeBindDataFieldNameEnum.NAME,  
9 - },  
10 -]; 3 +export const formSchemas: FormSchema[] = [];
1 -import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node';  
2 import { FormSchema } from '/@/components/Form'; 1 import { FormSchema } from '/@/components/Form';
  2 +import { isObject } from '/@/utils/is';
  3 +
  4 +export const validateJSON = (value: string): { value: Recordable | string; flag: boolean } => {
  5 + let flag = false;
  6 + try {
  7 + value = JSON.parse(value);
  8 + if (isObject(value)) flag = true;
  9 + } catch (error) {}
  10 + return { value, flag };
  11 +};
3 12
4 export const formSchemas: FormSchema[] = [ 13 export const formSchemas: FormSchema[] = [
5 { 14 {
6 - field: NodeBindDataFieldEnum.NAME,  
7 - component: 'Input',  
8 - label: NodeBindDataFieldNameEnum.NAME, 15 + field: 'value',
  16 + component: 'JSONEditor',
  17 + label: '配置',
  18 + valueField: 'value',
  19 + changeEvent: 'update:value',
  20 + rules: [
  21 + {
  22 + required: true,
  23 + validator: (_rule, value) => {
  24 + const { flag } = validateJSON(value);
  25 + return flag ? Promise.resolve() : Promise.reject('配置项不是一个JSON对象');
  26 + },
  27 + },
  28 + ],
  29 + componentProps: {
  30 + height: 300,
  31 + },
9 }, 32 },
10 ]; 33 ];
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 import type { CreateModalDefineExposeType } from '../../../types'; 2 import type { CreateModalDefineExposeType } from '../../../types';
3 import { BasicForm, useForm } from '/@/components/Form'; 3 import { BasicForm, useForm } from '/@/components/Form';
4 - import { formSchemas } from './create.config'; 4 + import { formSchemas, validateJSON } from './create.config';
5 import { NodeData } from '../../../types/node'; 5 import { NodeData } from '../../../types/node';
6 6
7 defineProps<{ 7 defineProps<{
@@ -16,12 +16,13 @@ @@ -16,12 +16,13 @@
16 const getValue: CreateModalDefineExposeType['getFieldsValue'] = async () => { 16 const getValue: CreateModalDefineExposeType['getFieldsValue'] = async () => {
17 await validate(); 17 await validate();
18 const value = getFieldsValue() || {}; 18 const value = getFieldsValue() || {};
19 - return value; 19 + const { value: data } = validateJSON(value?.value);
  20 + return data as Recordable;
20 }; 21 };
21 22
22 const setValue: CreateModalDefineExposeType['setFieldsValue'] = (value) => { 23 const setValue: CreateModalDefineExposeType['setFieldsValue'] = (value) => {
23 resetFields(); 24 resetFields();
24 - setFieldsValue(value); 25 + setFieldsValue({ value: JSON.stringify(value, null, 2) });
25 }; 26 };
26 27
27 defineExpose({ 28 defineExpose({
  1 +<script lang="ts" setup>
  2 + import { BasicModal, useModalInner } from '/@/components/Modal';
  3 + import ScriptFilterTest from './ScriptFilterTest.vue';
  4 + import { MessageTypesFilterEnum } from '../../../enum/form';
  5 + import { ref } from 'vue';
  6 + import { JavaScriptFunctionEditor } from '/@/components/Form';
  7 +
  8 + defineProps<{
  9 + visible?: boolean;
  10 + javaScriptEditorProps?: InstanceType<typeof JavaScriptFunctionEditor>['$props'];
  11 + }>();
  12 +
  13 + const emit = defineEmits(['save', 'register']);
  14 +
  15 + interface SaveValueType {
  16 + msg: Recordable;
  17 + metadata: Recordable;
  18 + msgType: MessageTypesFilterEnum;
  19 + javascriptFunction: string;
  20 + }
  21 +
  22 + const value = ref('');
  23 +
  24 + const [register, { closeModal }] = useModalInner((data: ModalParamsType<string>) => {
  25 + value.value = data.record;
  26 + });
  27 +
  28 + const handleSaveScript = (value: SaveValueType) => {
  29 + emit('save', value);
  30 + closeModal();
  31 + };
  32 +</script>
  33 +
  34 +<template>
  35 + <BasicModal
  36 + wrapClassName="scriptTestModal"
  37 + @register="register"
  38 + :visible="visible"
  39 + title="测试脚本功能"
  40 + defaultFullscreen
  41 + :showCancelBtn="false"
  42 + :showOkBtn="false"
  43 + :footer="null"
  44 + >
  45 + <ScriptFilterTest
  46 + :javaScriptEditorProps="javaScriptEditorProps"
  47 + :value="value"
  48 + @cancel="closeModal()"
  49 + @save="handleSaveScript"
  50 + />
  51 + </BasicModal>
  52 +</template>
  1 +<script lang="ts" setup>
  2 + import { Icon } from '/@/components/Icon';
  3 + import { Tooltip } from 'ant-design-vue';
  4 + import { ref } from 'vue';
  5 +
  6 + const visible = ref(false);
  7 + const handleOpenHelpMessage = () => {
  8 + visible.value = true;
  9 + };
  10 +</script>
  11 +
  12 +<template>
  13 + <Tooltip title="点击获取帮助">
  14 + <Icon
  15 + icon="material-symbols:help-outline"
  16 + class="cursor-pointer svg:text-xl"
  17 + @click="handleOpenHelpMessage"
  18 + />
  19 + </Tooltip>
  20 +</template>
  1 +<script setup lang="ts">
  2 + import { JavaScriptFunctionEditor } from '/@/components/Form';
  3 + import { Button } from 'ant-design-vue';
  4 + import FilterModal from './FilterModal.vue';
  5 + import { useModal } from '/@/components/Modal';
  6 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  7 + import { computed } from 'vue';
  8 +
  9 + const props = withDefaults(
  10 + defineProps<{
  11 + value?: string;
  12 + buttonName?: string;
  13 + javaScriptEditorProps?: InstanceType<typeof JavaScriptFunctionEditor>['$props'];
  14 + }>(),
  15 + {
  16 + buttonName: 'Test Filter Function',
  17 + javaScriptEditorProps: () => ({
  18 + functionName: 'Filter',
  19 + paramsName: ['msg', 'metadata', 'msgType'],
  20 + }),
  21 + }
  22 + );
  23 +
  24 + const emit = defineEmits(['update:value']);
  25 + const [register, { openModal }] = useModal();
  26 +
  27 + const handleOpen = () => {
  28 + openModal(true, {
  29 + record: props.value,
  30 + mode: DataActionModeEnum.CREATE,
  31 + } as ModalParamsType<string>);
  32 + };
  33 +
  34 + const getValue = computed<string>({
  35 + get() {
  36 + return props.value || '';
  37 + },
  38 + set(value) {
  39 + emit('update:value', value);
  40 + },
  41 + });
  42 +
  43 + const handleSave = (value: Record<'javascriptFunction', string>) => {
  44 + emit('update:value', value.javascriptFunction);
  45 + };
  46 +</script>
  47 +
  48 +<template>
  49 + <section>
  50 + <JavaScriptFunctionEditor v-model:value="getValue" v-bind="javaScriptEditorProps" />
  51 + <Button class="mt-4" type="primary" @click="handleOpen">{{ buttonName }}</Button>
  52 + <FilterModal
  53 + :javaScriptEditorProps="javaScriptEditorProps"
  54 + @register="register"
  55 + @save="handleSave"
  56 + />
  57 + </section>
  58 +</template>
  1 +<script lang="ts" setup>
  2 + import { Card, Select, Tag, Form, Button, Input } from 'ant-design-vue';
  3 + import { ref, unref, watch } from 'vue';
  4 + import { MessageTypesFilterEnum, MessageTypesFilterNameEnum } from '../../../enum/form';
  5 + import { JSONEditor } from '/@/components/CodeEditor';
  6 + import { buildShortUUID } from '/@/utils/uuid';
  7 + import { Icon } from '/@/components/Icon';
  8 + import { JavaScriptFunctionEditor } from '/@/components/Form';
  9 + import { useJsonParse } from '/@/hooks/business/useJsonParse';
  10 + import { useMessage } from '/@/hooks/web/useMessage';
  11 + import { isNullOrUnDef } from '/@/utils/is';
  12 +
  13 + interface MetaDataItemType {
  14 + label: string;
  15 + value: string;
  16 + uuid: string;
  17 + }
  18 +
  19 + interface Value {
  20 + msg: Recordable;
  21 + metadata: Recordable;
  22 + msgType: MessageTypesFilterEnum;
  23 + javascriptFunction: string;
  24 + }
  25 +
  26 + const props = withDefaults(
  27 + defineProps<{
  28 + value?: string;
  29 + javaScriptEditorProps?: InstanceType<typeof JavaScriptFunctionEditor>['$props'];
  30 + }>(),
  31 + {
  32 + javaScriptEditorProps: () => ({
  33 + functionName: 'Filter',
  34 + paramsName: ['msg', 'metadata', 'msgType'],
  35 + }),
  36 + }
  37 + );
  38 +
  39 + const emit = defineEmits<{
  40 + (eventName: 'cancel'): void;
  41 + (eventName: 'test', value: Value): void;
  42 + (eventName: 'save', value: Value): void;
  43 + }>();
  44 +
  45 + const messageType = ref(MessageTypesFilterEnum.POST_TELEMETRY);
  46 + const messageTypeOptions = Object.keys(MessageTypesFilterEnum).map((value) => ({
  47 + label: MessageTypesFilterNameEnum[value],
  48 + value,
  49 + }));
  50 +
  51 + const messageContent = ref({
  52 + temperature: 22.4,
  53 + humidity: 78,
  54 + });
  55 +
  56 + const scriptContent = ref('return msg.temperature > 20;');
  57 +
  58 + const outputContent = ref('');
  59 +
  60 + const metaData = ref<MetaDataItemType[]>([
  61 + { uuid: buildShortUUID(), label: 'deviceName', value: 'Test Device' },
  62 + { uuid: buildShortUUID(), label: 'deviceType', value: 'default' },
  63 + { uuid: buildShortUUID(), label: 'ts', value: Date.now().toString() },
  64 + ]);
  65 +
  66 + const handleAddMetadata = () => {
  67 + metaData.value.push({ uuid: buildShortUUID(), label: '', value: '' });
  68 + };
  69 +
  70 + const handleRemove = (data: MetaDataItemType) => {
  71 + const index = unref(metaData).findIndex((item) => item.uuid === data.uuid);
  72 + ~index && unref(metaData).splice(index, 1);
  73 + };
  74 +
  75 + const { createMessage } = useMessage();
  76 + const handleValidate = () => {
  77 + const message = unref(messageContent);
  78 + const metadata = unref(metaData);
  79 + const { flag } = useJsonParse(message);
  80 +
  81 + if (!flag) {
  82 + createMessage.warning('消息不是一个有效的JSON对象');
  83 + return false;
  84 + }
  85 +
  86 + for (const item of metadata) {
  87 + const { label, value } = item;
  88 + if (isNullOrUnDef(label) || isNullOrUnDef(value)) {
  89 + createMessage.warning('元数据的键名和键值为必填项');
  90 + return false;
  91 + }
  92 + }
  93 + return true;
  94 + };
  95 +
  96 + const getValue = (): Value => {
  97 + const msg = unref(messageContent);
  98 + const msgType = unref(messageType);
  99 + const metadata = unref(metaData).reduce(
  100 + (prev, next) => ({ ...prev, [next.label]: next.value }),
  101 + {} as Recordable
  102 + );
  103 + const javascriptFunction = unref(scriptContent);
  104 + return { msg, msgType, metadata, javascriptFunction };
  105 + };
  106 +
  107 + const handleTestScript = () => {
  108 + const flag = handleValidate();
  109 +
  110 + flag && emit('test', getValue());
  111 + };
  112 +
  113 + const handleSave = () => {
  114 + const flag = handleValidate();
  115 + flag && emit('save', getValue());
  116 + };
  117 +
  118 + watch(
  119 + () => props.value,
  120 + (value) => {
  121 + scriptContent.value = value || '';
  122 + }
  123 + );
  124 +</script>
  125 +
  126 +<template>
  127 + <section class="script-test-container w-full h-full flex flex-col">
  128 + <Form class="flex-auto grid grid-cols-2 grid-rows-2 bg-gray-100 gap-2">
  129 + <main>
  130 + <Card class="w-full h-full">
  131 + <div class="w-full h-full flex flex-col">
  132 + <div class="flex justify-between items-center">
  133 + <div class="flex flex-col">
  134 + <Form.Item label="消息类型" labelAlign="left" :colon="false">
  135 + <Select
  136 + v-model:value="messageType"
  137 + :options="messageTypeOptions"
  138 + :bordered="false"
  139 + class="min-w-60 border-b-gray-400 border-b"
  140 + />
  141 + </Form.Item>
  142 + </div>
  143 + <Tag color="blue"> 消息 </Tag>
  144 + </div>
  145 + <JSONEditor v-model:value="messageContent" title="消息" class="mt-4 flex-auto" />
  146 + </div>
  147 + </Card>
  148 + </main>
  149 + <main>
  150 + <Card class="w-full h-full">
  151 + <div class="flex flex-col">
  152 + <div class="flex justify-between">
  153 + <span class="text-gray-400">元数据</span>
  154 + <Tag color="blue">元数据</Tag>
  155 + </div>
  156 + <div class="mt-4 overflow-y-auto">
  157 + <div v-for="item in metaData" :key="item.uuid" class="flex mt-2 gap-2 items-end">
  158 + <Input v-model:value="item.label" placeholder="请输入键名" />
  159 + <Input v-model:value="item.value" placeholder="请输入键值" />
  160 + <Icon
  161 + icon="material-symbols:close"
  162 + class="svg:text-2xl cursor-pointer"
  163 + @click="handleRemove(item)"
  164 + />
  165 + </div>
  166 + </div>
  167 + <div class="mt-4">
  168 + <Button type="primary" @click="handleAddMetadata">添加</Button>
  169 + </div>
  170 + </div>
  171 + </Card>
  172 + </main>
  173 + <main>
  174 + <Card class="w-full h-full">
  175 + <JavaScriptFunctionEditor
  176 + v-model:value="scriptContent"
  177 + v-bind="javaScriptEditorProps"
  178 + height="100%"
  179 + >
  180 + <template #beforeFormat>
  181 + <Tag color="blue" class="!mr-0">{{ javaScriptEditorProps.functionName }}</Tag>
  182 + </template>
  183 + <template #beforeFullScreen>
  184 + <Icon icon="material-symbols:help-outline" class="cursor-pointer svg:text-xl" />
  185 + </template>
  186 + </JavaScriptFunctionEditor>
  187 + </Card>
  188 + </main>
  189 + <main>
  190 + <Card class="w-full h-full">
  191 + <JSONEditor
  192 + v-model:value="outputContent"
  193 + title="输出"
  194 + class="flex-auto"
  195 + height="100%"
  196 + :disabled="true"
  197 + >
  198 + <template #beforeFormat>
  199 + <Tag color="blue" class="!mr-0">输出</Tag>
  200 + </template>
  201 + </JSONEditor>
  202 + </Card>
  203 + </main>
  204 + </Form>
  205 + <div class="h-12 flex items-center justify-between px-4 bg-light-50">
  206 + <Button type="primary" @click="handleTestScript">测试</Button>
  207 + <div class="flex gap-4">
  208 + <Button @click="emit('cancel')">取消</Button>
  209 + <Button type="primary" @click="handleSave">保存</Button>
  210 + </div>
  211 + </div>
  212 + </section>
  213 +</template>
  214 +
  215 +<style lang="less" scoped>
  216 + .script-test-container {
  217 + :deep(.ant-card-body) {
  218 + @apply w-full h-full p-2;
  219 +
  220 + .ant-form-item {
  221 + label {
  222 + @apply text-xs pl-3 text-gray-400;
  223 + }
  224 + }
  225 +
  226 + .ant-input {
  227 + @apply !border-transparent !border-b !border-b-dark-300;
  228 +
  229 + &:focus {
  230 + @apply !shadow-none;
  231 + }
  232 + }
  233 + }
  234 + }
  235 +</style>
  1 +import { h } from 'vue';
1 import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node'; 2 import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node';
2 -import { FormSchema } from '/@/components/Form'; 3 +import { FormSchema, useComponentRegister } from '/@/components/Form';
  4 +import HelpMessage from './HelpMessage.vue';
  5 +import JavaScriptFilterFunctionModal from './JavaScriptFilterFunctionModal.vue';
  6 +
  7 +useComponentRegister('JavaScriptFilterFunctionModal', JavaScriptFilterFunctionModal);
3 8
4 export const formSchemas: FormSchema[] = [ 9 export const formSchemas: FormSchema[] = [
5 { 10 {
6 - field: NodeBindDataFieldEnum.NAME,  
7 - component: 'Input',  
8 - label: NodeBindDataFieldNameEnum.NAME, 11 + field: NodeBindDataFieldEnum.JS_SCRIPT,
  12 + component: 'JavaScriptFilterFunctionModal',
  13 + label: NodeBindDataFieldNameEnum.JS_SCRIPT,
  14 + changeEvent: 'update:value',
  15 + valueField: 'value',
  16 + renderComponentContent: () => {
  17 + return {
  18 + beforeFullScreen: () => h(HelpMessage),
  19 + };
  20 + },
9 }, 21 },
10 ]; 22 ];
1 import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node'; 1 import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node';
2 -import { FormSchema } from '/@/components/Form'; 2 +import { FormSchema, useComponentRegister } from '/@/components/Form';
  3 +import JavaScriptFilterFunctionModal from '../Script/JavaScriptFilterFunctionModal.vue';
3 4
  5 +useComponentRegister('JavaScriptFilterFunctionModal', JavaScriptFilterFunctionModal);
4 export const formSchemas: FormSchema[] = [ 6 export const formSchemas: FormSchema[] = [
5 { 7 {
6 - field: NodeBindDataFieldEnum.NAME,  
7 - component: 'Input',  
8 - label: NodeBindDataFieldNameEnum.NAME, 8 + field: NodeBindDataFieldEnum.JS_SCRIPT,
  9 + component: 'JavaScriptFilterFunctionModal',
  10 + label: NodeBindDataFieldNameEnum.JS_SCRIPT,
  11 + changeEvent: 'update:value',
  12 + valueField: 'value',
  13 + componentProps: {
  14 + buttonName: 'Test Switch Function',
  15 + javaScriptEditorProps: {
  16 + functionName: 'Switch',
  17 + paramsName: ['msg', 'metadata', 'mstType'],
  18 + },
  19 + },
  20 + renderComponentContent: () => {
  21 + return {
  22 + // beforeFullScreen: () => h(HelpMessage),
  23 + };
  24 + },
9 }, 25 },
10 ]; 26 ];
@@ -27,8 +27,8 @@ export const FilterComponents: NodeItemConfigType[] = [ @@ -27,8 +27,8 @@ export const FilterComponents: NodeItemConfigType[] = [
27 GpsGeofencingFilterConfig, 27 GpsGeofencingFilterConfig,
28 MessageTypeConfig, 28 MessageTypeConfig,
29 MessageTypeSwitchConfig, 29 MessageTypeSwitchConfig,
30 - OriginatorTypeSwitchConfig,  
31 OriginatorTypeConfig, 30 OriginatorTypeConfig,
  31 + OriginatorTypeSwitchConfig,
32 SceneReactConfig, 32 SceneReactConfig,
33 ScriptConfig, 33 ScriptConfig,
34 SwitchConfig, 34 SwitchConfig,
@@ -75,6 +75,7 @@ @@ -75,6 +75,7 @@
75 <section v-if="shadowComponent" class="w-full h-full"> 75 <section v-if="shadowComponent" class="w-full h-full">
76 <BasicForm @register="topFormRegister" /> 76 <BasicForm @register="topFormRegister" />
77 <component 77 <component
  78 + class="rule-node-form"
78 :is="shadowComponent" 79 :is="shadowComponent"
79 ref="createComponentEl" 80 ref="createComponentEl"
80 :key="getComponentKey" 81 :key="getComponentKey"
@@ -87,3 +88,12 @@ @@ -87,3 +88,12 @@
87 </Spin> 88 </Spin>
88 </BasicModal> 89 </BasicModal>
89 </template> 90 </template>
  91 +
  92 +<style lang="less" scoped>
  93 + .rule-node-form {
  94 + :deep(.ant-input-number) {
  95 + width: 100%;
  96 + min-width: none;
  97 + }
  98 + }
  99 +</style>
@@ -10,6 +10,6 @@ @@ -10,6 +10,6 @@
10 <p class="text-gray-600 italic"> 10 <p class="text-gray-600 italic">
11 {{ nodeData?.config?.configurationDescriptor.nodeDefinition.description }}</p 11 {{ nodeData?.config?.configurationDescriptor.nodeDefinition.description }}</p
12 > 12 >
13 - <p>{{ nodeData?.config?.configurationDescriptor.nodeDefinition.details }}</p> 13 + <p v-html="nodeData?.config?.configurationDescriptor.nodeDefinition.details"> </p>
14 </section> 14 </section>
15 </template> 15 </template>
@@ -99,6 +99,7 @@ @@ -99,6 +99,7 @@
99 <section v-if="shadowComponent" class="w-full h-full"> 99 <section v-if="shadowComponent" class="w-full h-full">
100 <BasicForm @register="topFormRegister" @vue:mounted="handleTopFormMounted" /> 100 <BasicForm @register="topFormRegister" @vue:mounted="handleTopFormMounted" />
101 <component 101 <component
  102 + class="rule-node-form"
102 :is="shadowComponent" 103 :is="shadowComponent"
103 ref="createComponentEl" 104 ref="createComponentEl"
104 :config="nodeData" 105 :config="nodeData"
@@ -119,3 +120,12 @@ @@ -119,3 +120,12 @@
119 </Tabs> 120 </Tabs>
120 </BasicDrawer> 121 </BasicDrawer>
121 </template> 122 </template>
  123 +
  124 +<style lang="less" scoped>
  125 + .rule-node-form {
  126 + :deep(.ant-input-number) {
  127 + width: 100%;
  128 + min-width: none;
  129 + }
  130 + }
  131 +</style>
@@ -136,7 +136,6 @@ export const tenantFormSchema: FormSchema[] = [ @@ -136,7 +136,6 @@ export const tenantFormSchema: FormSchema[] = [
136 } 136 }
137 return { items, total }; 137 return { items, total };
138 }, 138 },
139 - showSearch: true,  
140 labelField: 'name', 139 labelField: 'name',
141 valueField: 'id.id', 140 valueField: 'id.id',
142 filterOption: (inputValue: string, options: Record<'label' | 'value', string>) => { 141 filterOption: (inputValue: string, options: Record<'label' | 'value', string>) => {
@@ -49,3 +49,10 @@ export interface PaginationResult<T = Recordable> { @@ -49,3 +49,10 @@ export interface PaginationResult<T = Recordable> {
49 items: T[]; 49 items: T[];
50 total: number; 50 total: number;
51 } 51 }
  52 +
  53 +export interface TBPaginationResult<T = Recordable> {
  54 + data: T[];
  55 + hasNext: boolean;
  56 + totalElements: number;
  57 + totalPages: number;
  58 +}