Showing
42 changed files
with
1388 additions
and
132 deletions
src/api/ruleChainDesigner/index.ts
0 → 100644
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 | +}; | ... | ... |
src/api/ruleChainDesigner/model/index.ts
0 → 100644
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 | 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 | 12 | enum EventEnum { |
11 | 13 | UPDATE_VALUE = 'update:value', |
... | ... | @@ -16,89 +18,133 @@ |
16 | 18 | |
17 | 19 | const props = withDefaults( |
18 | 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 | 27 | height: 150, |
31 | 28 | } |
32 | 29 | ); |
33 | 30 | |
34 | 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 | 38 | const jsonEditorElRef = ref<Nullable<any>>(); |
42 | 39 | |
43 | - const editoreRef = ref<JSONEditor>(); | |
40 | + const editoreRef = ref<Ace.Editor>(); | |
44 | 41 | |
45 | 42 | const isFocus = ref(false); |
46 | 43 | |
47 | - const handleChange = (value: any) => { | |
44 | + const handleOnChange = () => { | |
45 | + const value = get(); | |
48 | 46 | emit(EventEnum.UPDATE_VALUE, value, unref(editoreRef)); |
49 | 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 | 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 | 84 | watch( |
77 | 85 | () => props.value, |
78 | 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 | 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 | 104 | const get = (): string => { |
88 | - return unref(editoreRef)?.getText() || ''; | |
105 | + return unref(editoreRef)?.getValue() || ''; | |
89 | 106 | }; |
90 | 107 | |
91 | 108 | const set = (data: any) => { |
92 | - return unref(editoreRef)?.set(data); | |
109 | + return unref(editoreRef)?.setValue(getFormatValue(data)); | |
93 | 110 | }; |
94 | 111 | |
95 | 112 | onMounted(() => { |
96 | 113 | initialize(); |
97 | - unref(editoreRef)?.setText(props.value || ''); | |
114 | + unref(editoreRef)?.setValue(getFormatValue(props.value)); | |
98 | 115 | }); |
99 | 116 | |
100 | 117 | onUnmounted(() => { |
118 | + unref(editoreRef)?.off('change', handleOnChange); | |
119 | + unref(editoreRef)?.off('blur', handleOnBlur); | |
120 | + unref(editoreRef)?.off('focus', handleOnFocus); | |
101 | 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 | 150 | defineExpose({ |
... | ... | @@ -108,22 +154,41 @@ |
108 | 154 | </script> |
109 | 155 | |
110 | 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 | 193 | </div> |
114 | 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 | 13 | export { default as ApiUpload } from './src/components/ApiUpload.vue'; |
14 | 14 | |
15 | 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 | 19 | export { | ... | ... |
... | ... | @@ -6,7 +6,7 @@ |
6 | 6 | <script lang="ts" setup> |
7 | 7 | import { ref, watchEffect, computed, unref, watch, reactive } from 'vue'; |
8 | 8 | import { Select, Spin } from 'ant-design-vue'; |
9 | - import { isFunction } from '/@/utils/is'; | |
9 | + import { isFunction, isNullAndUnDef } from '/@/utils/is'; | |
10 | 10 | import { useRuleFormItem } from '/@/hooks/component/useFormItem'; |
11 | 11 | import { useAttrs } from '/@/hooks/core/useAttrs'; |
12 | 12 | import { get, omit } from 'lodash-es'; |
... | ... | @@ -30,8 +30,11 @@ |
30 | 30 | labelField?: string; |
31 | 31 | valueField?: string; |
32 | 32 | immediate?: boolean; |
33 | - pagenation?: Pagination; | |
33 | + searchField?: string; | |
34 | + pagination?: Pagination; | |
34 | 35 | queryEmptyDataAgin?: boolean; |
36 | + fetchSearch?: boolean; | |
37 | + filterOption?: (inputValue: string, options: Recordable) => boolean; | |
35 | 38 | }>(), |
36 | 39 | { |
37 | 40 | resultField: '', |
... | ... | @@ -39,14 +42,15 @@ |
39 | 42 | valueField: 'value', |
40 | 43 | immediate: true, |
41 | 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 | 50 | const OptionsItem = (_, { attrs }: { attrs: { vNode: any } }) => attrs.vNode; |
47 | 51 | |
48 | 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 | 54 | const scrollLoading = ref(false); |
51 | 55 | const lock = ref(false); |
52 | 56 | const loading = ref(false); |
... | ... | @@ -55,6 +59,13 @@ |
55 | 59 | const attrs = useAttrs(); |
56 | 60 | const { t } = useI18n(); |
57 | 61 | |
62 | + const getPagination = computed(() => { | |
63 | + return { | |
64 | + ...props.pagination, | |
65 | + ...unref(pagination), | |
66 | + }; | |
67 | + }); | |
68 | + | |
58 | 69 | // Embedded in the form, just use the hook binding to perform form verification |
59 | 70 | const [state] = useRuleFormItem(props, 'value', 'change', emitData); |
60 | 71 | |
... | ... | @@ -86,16 +97,18 @@ |
86 | 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 | 102 | if (!api || !isFunction(api)) return; |
103 | + const isFetchSearchFlag = fetchSearch && !isNullAndUnDef(searchText) && searchField; | |
92 | 104 | try { |
93 | 105 | !unref(getOptions).length ? (loading.value = true) : (scrollLoading.value = true); |
94 | 106 | lock.value = true; |
95 | 107 | const { total, items } = await api({ |
96 | 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 | 114 | pagination.total = total; |
... | ... | @@ -105,11 +118,13 @@ |
105 | 118 | return; |
106 | 119 | } |
107 | 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 | 125 | emitChange(); |
111 | 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 | 128 | console.warn(error); |
114 | 129 | } finally { |
115 | 130 | isFirstLoad.value = false; |
... | ... | @@ -134,17 +149,39 @@ |
134 | 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 | 160 | async function handlePopupScroll(event: MouseEvent) { |
138 | 161 | const { scrollHeight, scrollTop, clientHeight } = event.target as HTMLDivElement; |
139 | 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 | 167 | await fetch(); |
143 | 168 | } |
144 | 169 | } |
145 | 170 | } |
146 | 171 | |
147 | 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 | 185 | </script> |
149 | 186 | |
150 | 187 | <template> |
... | ... | @@ -153,6 +190,8 @@ |
153 | 190 | v-bind="attrs" |
154 | 191 | @change="handleChange" |
155 | 192 | :options="getOptions" |
193 | + :filterOption="handleFilterOption" | |
194 | + :showSearch="true" | |
156 | 195 | v-model:value="state" |
157 | 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> | ... | ... |
... | ... | @@ -19,4 +19,8 @@ export enum DictEnum { |
19 | 19 | DISABLE_CUSTOMER_AUTH = 'disabled_customer_auth', |
20 | 20 | // 寄存器数据格式 |
21 | 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 | } | ... | ... |
src/hooks/business/useBatchSettingDict.ts
0 → 100644
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 | +} | ... | ... |
src/hooks/business/useBeautify.ts
0 → 100644
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 | +} | ... | ... |
src/hooks/business/useJsonParse.ts
0 → 100644
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 | 91 | valueField: 'fileList', |
92 | 92 | componentProps: () => { |
93 | 93 | return { |
94 | - listType: 'picture-card', | |
94 | + // listType: 'picture-card', | |
95 | 95 | maxFileLimit: 1, |
96 | 96 | api: async (file: File) => { |
97 | 97 | try { |
... | ... | @@ -110,6 +110,10 @@ export const formSchema: FormSchema[] = [ |
110 | 110 | onPreview: (fileList: FileItem) => { |
111 | 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 | 46 | valueField: 'fileList', |
47 | 47 | componentProps: () => { |
48 | 48 | return { |
49 | - listType: 'picture-card', | |
49 | + // listType: 'picture-card', | |
50 | 50 | maxFileLimit: 1, |
51 | 51 | api: async (file: File) => { |
52 | 52 | try { |
... | ... | @@ -62,6 +62,10 @@ export const formSchema: FormSchema[] = [ |
62 | 62 | return {}; |
63 | 63 | } |
64 | 64 | }, |
65 | + showUploadList: true, | |
66 | + onDownload(file) { | |
67 | + console.log(file); | |
68 | + }, | |
65 | 69 | onPreview: (fileList: FileItem) => { |
66 | 70 | createImgPreview({ imageList: [fileList.url!] }); |
67 | 71 | }, | ... | ... |
... | ... | @@ -23,7 +23,7 @@ |
23 | 23 | layout: 'vertical', |
24 | 24 | }); |
25 | 25 | |
26 | - const { genForm } = useGenDynamicForm(); | |
26 | + const { genForm, transformValue } = useGenDynamicForm(); | |
27 | 27 | |
28 | 28 | const keys = ref<string[]>([]); |
29 | 29 | |
... | ... | @@ -127,8 +127,8 @@ |
127 | 127 | |
128 | 128 | sendValue.value = await genModbusCommand(unref(modBUSForm)); |
129 | 129 | } else { |
130 | - const _value = getFieldsValue(); | |
131 | - | |
130 | + await validate(); | |
131 | + const _value = transformValue(getFieldsValue()); | |
132 | 132 | sendValue.value = unref(keys).reduce((prev, next) => { |
133 | 133 | return { ...prev, [next]: _value[next] }; |
134 | 134 | }, {}); | ... | ... |
1 | 1 | import { DataType, Specs, StructJSON } from '/@/api/device/model/modelOfMatterModel'; |
2 | 2 | import { JSONEditor } from '/@/components/CodeEditor'; |
3 | 3 | import { FormSchema, useComponentRegister } from '/@/components/Form'; |
4 | +import { useJsonParse } from '/@/hooks/business/useJsonParse'; | |
4 | 5 | import { DataTypeEnum } from '/@/views/device/profiles/step/cpns/physical/cpns/config'; |
5 | 6 | |
6 | 7 | export interface BasicCreateFormParams { |
... | ... | @@ -20,7 +21,6 @@ export const useGenDynamicForm = () => { |
20 | 21 | const { specs, type } = dataType; |
21 | 22 | const { valueRange, step } = specs! as Partial<Specs>; |
22 | 23 | const { max, min } = valueRange || {}; |
23 | - console.log({ max, min }); | |
24 | 24 | return { |
25 | 25 | field: identifier, |
26 | 26 | label: functionName, |
... | ... | @@ -107,6 +107,19 @@ export const useGenDynamicForm = () => { |
107 | 107 | field: identifier, |
108 | 108 | label: functionName, |
109 | 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 | 131 | [DataTypeEnum.IS_STRUCT]: createInputJson, |
119 | 132 | }; |
120 | 133 | |
134 | + const fieldTypeMap = new Map<string, DataTypeEnum>(); | |
121 | 135 | const genForm = (schemas: StructJSON[]) => { |
136 | + fieldTypeMap.clear(); | |
122 | 137 | const formSchema = schemas.map((item) => { |
123 | 138 | const { functionName, identifier, dataType } = item; |
124 | 139 | const { type } = dataType || {}; |
125 | 140 | |
141 | + fieldTypeMap.set(identifier!, dataType!.type); | |
126 | 142 | const method = schemaMethod[type!]; |
127 | 143 | |
128 | 144 | const formSchema = method?.({ |
... | ... | @@ -137,5 +153,21 @@ export const useGenDynamicForm = () => { |
137 | 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 | }; | ... | ... |
src/views/rule/designer/enum/form.ts
0 → 100644
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 | 5 | |
6 | 6 | export enum NodeBindDataFieldEnum { |
7 | 7 | NAME = 'name', |
8 | - ALARM_STATUS_LIST = 'alarmStatusList', | |
9 | 8 | DESCRIPTION = 'description', |
10 | 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 | 48 | export enum NodeBindDataFieldNameEnum { |
14 | 49 | NAME = '名称', |
15 | - ALARM_STATUS_LIST = 'Alarm status filter', | |
16 | 50 | DESCRIPTION = '说明', |
17 | 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 | 89 | export enum EdgeBindDataFieldEnum { | ... | ... |
... | ... | @@ -53,7 +53,7 @@ export function useBasicDataTransform() { |
53 | 53 | |
54 | 54 | nodes.forEach((item, index) => indexMap.set(index, item)); |
55 | 55 | |
56 | - connections.forEach((item) => { | |
56 | + (connections || []).forEach((item) => { | |
57 | 57 | const { fromIndex, toIndex, type } = item; |
58 | 58 | const key = `${fromIndex}${SEPARATOR}${toIndex}`; |
59 | 59 | if (!groupByConnections.has(key)) groupByConnections.set(key, []); | ... | ... |
... | ... | @@ -124,7 +124,7 @@ export function useSaveAndRedo() { |
124 | 124 | firstNodeIndex, |
125 | 125 | ruleChainId: { |
126 | 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 | 140 | try { |
141 | 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 | 145 | const data = await getRuleChainData(id); |
146 | 146 | ... | ... |
... | ... | @@ -3,8 +3,27 @@ import { FormSchema } from '/@/components/Form'; |
3 | 3 | |
4 | 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 | 8 | import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node'; |
9 | +import { deviceProfilePage } from '/@/api/device/deviceManager'; | |
2 | 10 | import { FormSchema } from '/@/components/Form'; |
3 | 11 | |
4 | 12 | export const formSchemas: FormSchema[] = [ |
... | ... | @@ -7,4 +15,64 @@ export const formSchemas: FormSchema[] = [ |
7 | 15 | component: 'Input', |
8 | 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 | 2 | import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node'; |
2 | 3 | import { FormSchema } from '/@/components/Form'; |
3 | 4 | |
4 | 5 | export const formSchemas: FormSchema[] = [ |
5 | 6 | { |
6 | - field: NodeBindDataFieldEnum.NAME, | |
7 | + field: NodeBindDataFieldEnum.LATITUDE_KEY_NAME, | |
7 | 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 | 1 | import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node'; |
2 | +import { findDictItemByCode } from '/@/api/system/dict'; | |
2 | 3 | import { FormSchema } from '/@/components/Form'; |
4 | +import { DictEnum } from '/@/enums/dictEnum'; | |
3 | 5 | |
4 | 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 | 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 | 1 | import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node'; |
2 | +import { findDictItemByCode } from '/@/api/system/dict'; | |
2 | 3 | import { FormSchema } from '/@/components/Form'; |
4 | +import { DictEnum } from '/@/enums/dictEnum'; | |
3 | 5 | |
4 | 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 | 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 | 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 | 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 | 1 | <script lang="ts" setup> |
2 | 2 | import type { CreateModalDefineExposeType } from '../../../types'; |
3 | 3 | import { BasicForm, useForm } from '/@/components/Form'; |
4 | - import { formSchemas } from './create.config'; | |
4 | + import { formSchemas, validateJSON } from './create.config'; | |
5 | 5 | import { NodeData } from '../../../types/node'; |
6 | 6 | |
7 | 7 | defineProps<{ |
... | ... | @@ -16,12 +16,13 @@ |
16 | 16 | const getValue: CreateModalDefineExposeType['getFieldsValue'] = async () => { |
17 | 17 | await validate(); |
18 | 18 | const value = getFieldsValue() || {}; |
19 | - return value; | |
19 | + const { value: data } = validateJSON(value?.value); | |
20 | + return data as Recordable; | |
20 | 21 | }; |
21 | 22 | |
22 | 23 | const setValue: CreateModalDefineExposeType['setFieldsValue'] = (value) => { |
23 | 24 | resetFields(); |
24 | - setFieldsValue(value); | |
25 | + setFieldsValue({ value: JSON.stringify(value, null, 2) }); | |
25 | 26 | }; |
26 | 27 | |
27 | 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 | 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 | 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 | 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 | 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 | 27 | GpsGeofencingFilterConfig, |
28 | 28 | MessageTypeConfig, |
29 | 29 | MessageTypeSwitchConfig, |
30 | - OriginatorTypeSwitchConfig, | |
31 | 30 | OriginatorTypeConfig, |
31 | + OriginatorTypeSwitchConfig, | |
32 | 32 | SceneReactConfig, |
33 | 33 | ScriptConfig, |
34 | 34 | SwitchConfig, | ... | ... |
... | ... | @@ -75,6 +75,7 @@ |
75 | 75 | <section v-if="shadowComponent" class="w-full h-full"> |
76 | 76 | <BasicForm @register="topFormRegister" /> |
77 | 77 | <component |
78 | + class="rule-node-form" | |
78 | 79 | :is="shadowComponent" |
79 | 80 | ref="createComponentEl" |
80 | 81 | :key="getComponentKey" |
... | ... | @@ -87,3 +88,12 @@ |
87 | 88 | </Spin> |
88 | 89 | </BasicModal> |
89 | 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 | 10 | <p class="text-gray-600 italic"> |
11 | 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 | 14 | </section> |
15 | 15 | </template> | ... | ... |
... | ... | @@ -99,6 +99,7 @@ |
99 | 99 | <section v-if="shadowComponent" class="w-full h-full"> |
100 | 100 | <BasicForm @register="topFormRegister" @vue:mounted="handleTopFormMounted" /> |
101 | 101 | <component |
102 | + class="rule-node-form" | |
102 | 103 | :is="shadowComponent" |
103 | 104 | ref="createComponentEl" |
104 | 105 | :config="nodeData" |
... | ... | @@ -119,3 +120,12 @@ |
119 | 120 | </Tabs> |
120 | 121 | </BasicDrawer> |
121 | 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 | 136 | } |
137 | 137 | return { items, total }; |
138 | 138 | }, |
139 | - showSearch: true, | |
140 | 139 | labelField: 'name', |
141 | 140 | valueField: 'id.id', |
142 | 141 | filterOption: (inputValue: string, options: Record<'label' | 'value', string>) => { | ... | ... |