Showing
67 changed files
with
4066 additions
and
601 deletions
Too many changes to show.
To preserve performance only 67 of 144 files are displayed.
... | ... | @@ -5,7 +5,9 @@ |
5 | 5 | "public/resource/tinymce/langs" |
6 | 6 | ], |
7 | 7 | "cSpell.words": [ |
8 | + "Cmds", | |
8 | 9 | "COAP", |
10 | + "echarts", | |
9 | 11 | "edrx", |
10 | 12 | "EFENTO", |
11 | 13 | "inited", |
... | ... | @@ -17,6 +19,8 @@ |
17 | 19 | "unref", |
18 | 20 | "vben", |
19 | 21 | "VITE", |
22 | + "vnode", | |
23 | + "vueuse", | |
20 | 24 | "windicss" |
21 | 25 | ] |
22 | 26 | } | ... | ... |
1 | 1 | <script lang="ts" setup> |
2 | 2 | import '@simonwep/pickr/dist/themes/monolith.min.css'; |
3 | 3 | import ColorPicker from '@simonwep/pickr'; |
4 | - import type { PropType } from 'vue'; | |
4 | + import { PropType, watch } from 'vue'; | |
5 | 5 | import { computed, onMounted, onUnmounted, ref, unref } from 'vue'; |
6 | 6 | |
7 | 7 | type Format = Exclude<keyof ColorPicker.HSVaColor, 'clone'>; |
... | ... | @@ -18,10 +18,6 @@ |
18 | 18 | config: { |
19 | 19 | type: Object as PropType<ColorPicker.Options>, |
20 | 20 | }, |
21 | - defaultValue: { | |
22 | - type: String, | |
23 | - default: '', | |
24 | - }, | |
25 | 21 | }); |
26 | 22 | |
27 | 23 | const emit = defineEmits(['update:value']); |
... | ... | @@ -52,11 +48,16 @@ |
52 | 48 | }; |
53 | 49 | |
54 | 50 | const onClear = () => { |
55 | - emit('update:value', props.defaultValue); | |
51 | + emit('update:value', props.value); | |
56 | 52 | unref(picker)?.hide(); |
57 | - unref(picker)?.setColor(props.defaultValue); | |
53 | + props.value && unref(picker)?.setColor(props.value); | |
58 | 54 | }; |
59 | 55 | |
56 | + // const onChange = () => { | |
57 | + // const value = getColor(); | |
58 | + // emit('update:value', value); | |
59 | + // }; | |
60 | + | |
60 | 61 | const getOption = computed<ColorPicker.Options>(() => { |
61 | 62 | const { config = {} } = props; |
62 | 63 | return { |
... | ... | @@ -100,17 +101,28 @@ |
100 | 101 | }; |
101 | 102 | }); |
102 | 103 | |
104 | + watch( | |
105 | + () => props.value, | |
106 | + (value) => { | |
107 | + if (value) { | |
108 | + unref(picker)?.setColor(value); | |
109 | + } | |
110 | + } | |
111 | + ); | |
112 | + | |
103 | 113 | onMounted(() => { |
104 | 114 | picker.value = ColorPicker.create(unref(getOption)); |
105 | 115 | unref(picker)?.on('init', onInit); |
106 | 116 | unref(picker)?.on('save', onSave); |
107 | 117 | unref(picker)?.on('clear', onClear); |
118 | + // unref(picker)?.on('change', onChange); | |
108 | 119 | }); |
109 | 120 | |
110 | 121 | onUnmounted(() => { |
111 | 122 | unref(picker)?.off('init', onInit); |
112 | 123 | unref(picker)?.off('save', onSave); |
113 | 124 | unref(picker)?.off('clear', onClear); |
125 | + // unref(picker)?.off('change', onChange); | |
114 | 126 | |
115 | 127 | unref(picker)?.destroyAndRemove(); |
116 | 128 | }); | ... | ... |
src/hooks/web/useInjectScript.ts
0 → 100644
1 | +import { onUnmounted, ref } from 'vue'; | |
2 | +import { isFunction } from '/@/utils/is'; | |
3 | + | |
4 | +interface ScriptOptions { | |
5 | + src: string; | |
6 | + onLoad?: Fn; | |
7 | + onError?: Fn; | |
8 | +} | |
9 | + | |
10 | +export enum InjectScriptStatusEnum { | |
11 | + DONE = 'DONE', | |
12 | + LOAD = 'LOAD', | |
13 | + SUCCESS = 'SUCCESS', | |
14 | + ERROR = 'ERROR', | |
15 | +} | |
16 | + | |
17 | +export function useInjectScript(opts: ScriptOptions) { | |
18 | + const loading = ref(false); | |
19 | + const status = ref(InjectScriptStatusEnum.DONE); | |
20 | + let script: HTMLScriptElement; | |
21 | + | |
22 | + const toInject = () => { | |
23 | + loading.value = true; | |
24 | + const { onError, onLoad } = opts; | |
25 | + status.value = InjectScriptStatusEnum.LOAD; | |
26 | + return new Promise((resolve, reject) => { | |
27 | + script = document.createElement('script'); | |
28 | + script.type = 'text/javascript'; | |
29 | + script.onload = function () { | |
30 | + loading.value = false; | |
31 | + status.value = InjectScriptStatusEnum.SUCCESS; | |
32 | + onLoad && isFunction(onLoad) && onLoad(); | |
33 | + resolve(''); | |
34 | + }; | |
35 | + | |
36 | + script.onerror = function (err) { | |
37 | + loading.value = false; | |
38 | + status.value = InjectScriptStatusEnum.ERROR; | |
39 | + onError && isFunction(onError) && onError(); | |
40 | + reject(err); | |
41 | + }; | |
42 | + | |
43 | + script.src = opts.src; | |
44 | + document.head.appendChild(script); | |
45 | + }); | |
46 | + }; | |
47 | + | |
48 | + onUnmounted(() => { | |
49 | + script && script.remove(); | |
50 | + }); | |
51 | + | |
52 | + return { | |
53 | + loading, | |
54 | + status, | |
55 | + toInject, | |
56 | + }; | |
57 | +} | ... | ... |
... | ... | @@ -102,7 +102,10 @@ export const basicSchema: FormSchema[] = [ |
102 | 102 | }, |
103 | 103 | ]; |
104 | 104 | |
105 | -export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): FormSchema[] => { | |
105 | +export const dataSourceSchema = (isEdit: boolean, frontId?: string): FormSchema[] => { | |
106 | + // console.log(useSelectWidgetKeys()); | |
107 | + // const isEdit = unref(useSelectWidgetMode()) === DataActionModeEnum.UPDATE; | |
108 | + | |
106 | 109 | return [ |
107 | 110 | { |
108 | 111 | field: DataSourceField.IS_GATEWAY_DEVICE, | ... | ... |
1 | 1 | <script lang="ts" setup> |
2 | - import { Button, PageHeader, Empty, Spin, Tooltip } from 'ant-design-vue'; | |
3 | - import { GridItem, GridLayout } from 'vue3-grid-layout'; | |
4 | - import { nextTick, onMounted, ref } from 'vue'; | |
5 | - import WidgetWrapper from '../components/WidgetWrapper/WidgetWrapper.vue'; | |
6 | - import BaseWidgetHeader from '../components/WidgetHeader/BaseWidgetHeader.vue'; | |
7 | - import { DropMenu } from '/@/components/Dropdown'; | |
8 | - import DataBindModal from './components/DataBindModal.vue'; | |
9 | - import { useModal } from '/@/components/Modal'; | |
10 | - import { | |
11 | - decode, | |
12 | - DEFAULT_MAX_COL, | |
13 | - DEFAULT_MIN_HEIGHT, | |
14 | - DEFAULT_MIN_WIDTH, | |
15 | - DEFAULT_WIDGET_HEIGHT, | |
16 | - DEFAULT_WIDGET_WIDTH, | |
17 | - MoreActionEvent, | |
18 | - VisualComponentPermission, | |
19 | - } from '../config/config'; | |
20 | - import { | |
21 | - addDataComponent, | |
22 | - deleteDataComponent, | |
23 | - getDataComponent, | |
24 | - updateDataBoardLayout, | |
25 | - } from '/@/api/dataBoard'; | |
26 | - import { useRoute, useRouter } from 'vue-router'; | |
27 | - import { computed, unref } from '@vue/reactivity'; | |
28 | - import { | |
29 | - ComponentInfoDetail, | |
30 | - DataComponentRecord, | |
31 | - DataSource, | |
32 | - Layout, | |
33 | - } from '/@/api/dataBoard/model'; | |
34 | - import { frontComponentMap } from '../components/help'; | |
35 | - import { calcScale } from './config/util'; | |
36 | - import { useMessage } from '/@/hooks/web/useMessage'; | |
37 | - import { DataBoardLayoutInfo } from '../types/type'; | |
38 | - import Authority from '/@/components/Authority/src/Authority.vue'; | |
39 | - import { useSocketConnect } from '../hook/useSocketConnect'; | |
40 | - import { buildUUID } from '/@/utils/uuid'; | |
41 | - import HistoryTrendModal from './components/HistoryTrendModal.vue'; | |
42 | - import trendIcon from '/@/assets/svg/trend.svg'; | |
43 | - import backIcon from '/@/assets/images/back.png'; | |
44 | - import backWhiteIcon from '/@/assets/images/backWhite.png'; | |
45 | - import { useCalcGridLayout } from '../hook/useCalcGridLayout'; | |
46 | - import { FrontComponent } from '../const/const'; | |
47 | - import { useScript } from '/@/hooks/web/useScript'; | |
48 | - import { BAI_DU_MAP_GL_LIB, BAI_DU_MAP_TRACK_ANIMATION } from '/@/utils/fnUtils'; | |
49 | - import { useAppStore } from '/@/store/modules/app'; | |
50 | - import { useRole } from '/@/hooks/business/useRole'; | |
51 | - | |
52 | - const userStore = useAppStore(); | |
53 | - | |
54 | - const getAceClass = computed((): string => userStore.getDarkMode); | |
55 | - | |
56 | - const props = defineProps<{ | |
57 | - value?: Recordable; | |
58 | - }>(); | |
59 | - | |
60 | - const ROUTE = useRoute(); | |
61 | - | |
62 | - const ROUTER = useRouter(); | |
63 | - | |
64 | - const { isCustomerUser } = useRole(); | |
65 | - const { toPromise: injectBaiDuMapLib } = useScript({ src: BAI_DU_MAP_GL_LIB }); | |
66 | - const { toPromise: injectBaiDuMapTrackAniMationLib } = useScript({ | |
67 | - src: BAI_DU_MAP_TRACK_ANIMATION, | |
68 | - }); | |
69 | - | |
70 | - const { createMessage, createConfirm } = useMessage(); | |
71 | - | |
72 | - const getBoardId = computed(() => { | |
73 | - return decode((ROUTE.params as { boardId: string }).boardId); | |
74 | - }); | |
75 | - | |
76 | - const getDataBoardName = computed(() => { | |
77 | - return decode((ROUTE.params as { boardName: string }).boardName || ''); | |
78 | - }); | |
79 | - | |
80 | - const getSharePageData = computed(() => { | |
81 | - return props.value!; | |
82 | - }); | |
83 | - | |
84 | - const getIsSharePage = computed(() => { | |
85 | - return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId'); | |
86 | - }); | |
87 | - | |
88 | - const widgetEl = new Map<string, Fn>(); | |
89 | - | |
90 | - const dataBoardList = ref<DataBoardLayoutInfo[]>([]); | |
91 | - | |
92 | - const draggable = ref(!unref(getIsSharePage)); | |
93 | - const resizable = ref(!unref(getIsSharePage)); | |
94 | - | |
95 | - const GirdLayoutColNum = DEFAULT_MAX_COL; | |
96 | - const GridLayoutMargin = 20; | |
97 | - | |
98 | - const handleBack = () => { | |
99 | - if (unref(getIsSharePage)) return; | |
100 | - ROUTER.go(-1); | |
101 | - }; | |
102 | - | |
103 | - function updateSize(i: string, _newH: number, _newW: number, newHPx: number, newWPx: number) { | |
104 | - newWPx = Number(newWPx); | |
105 | - newHPx = Number(newHPx); | |
106 | - | |
107 | - const data = dataBoardList.value.find((item) => item.i === i)!; | |
108 | - const length = data.record.dataSource.length || 0; | |
109 | - | |
110 | - const row = Math.floor(Math.pow(length, 0.5)); | |
111 | - const col = Math.floor(length / row); | |
112 | - let width = Math.floor(100 / col); | |
113 | - let height = Math.floor(100 / row); | |
114 | - | |
115 | - const WHRatio = newWPx / newHPx; | |
116 | - const HWRatio = newHPx / newWPx; | |
117 | - | |
118 | - if (WHRatio > 1.6) { | |
119 | - width = Math.floor(100 / length); | |
120 | - height = 100; | |
121 | - } | |
122 | - | |
123 | - if (HWRatio > 1.6) { | |
124 | - height = Math.floor(100 / length); | |
125 | - width = 100; | |
126 | - } | |
127 | - | |
128 | - data.width = newWPx; | |
129 | - data.height = newHPx; | |
130 | - | |
131 | - data.record.dataSource = data.record.dataSource.map((item) => { | |
132 | - if (!item.uuid) item.uuid = buildUUID(); | |
133 | - return { | |
134 | - ...item, | |
135 | - width, | |
136 | - height, | |
137 | - radio: calcScale(newWPx, newHPx, width, height), | |
138 | - }; | |
139 | - }); | |
140 | - | |
141 | - nextTick(() => { | |
142 | - const updateFn = widgetEl.get(i); | |
143 | - if (updateFn) updateFn(); | |
144 | - }); | |
145 | - } | |
146 | - | |
147 | - const itemResize = (i: string, newH: number, newW: number, newHPx: number, newWPx: number) => { | |
148 | - updateSize(i, newH, newW, newHPx, newWPx); | |
149 | - }; | |
150 | - | |
151 | - const itemResized = (i: string, newH: number, newW: number, newHPx: number, newWPx: number) => { | |
152 | - handleSaveLayoutInfo(); | |
153 | - updateSize(i, newH, newW, newHPx, newWPx); | |
154 | - }; | |
155 | - | |
156 | - const itemContainerResized = ( | |
157 | - i: string, | |
158 | - newH: number, | |
159 | - newW: number, | |
160 | - newHPx: number, | |
161 | - newWPx: number | |
162 | - ) => { | |
163 | - updateSize(i, newH, newW, newHPx, newWPx); | |
164 | - }; | |
165 | - | |
166 | - const itemMoved = (i: string) => { | |
167 | - handleSaveLayoutInfo(); | |
168 | - updateCharts(i); | |
169 | - }; | |
170 | - | |
171 | - const updateCharts = (i: string) => { | |
172 | - nextTick(() => { | |
173 | - const updateFn = widgetEl.get(i); | |
174 | - if (updateFn) updateFn(); | |
175 | - }); | |
176 | - }; | |
177 | - | |
178 | - const setComponentRef = (el: Element, record: DataBoardLayoutInfo) => { | |
179 | - if (widgetEl.has(record.i)) widgetEl.delete(record.i); | |
180 | - if (el && (el as unknown as { update: Fn }).update) | |
181 | - widgetEl.set(record.i, (el as unknown as { update: Fn }).update); | |
182 | - }; | |
183 | - | |
184 | - const [register, { openModal }] = useModal(); | |
185 | - | |
186 | - const handleMoreAction = (event: DropMenu, id: string) => { | |
187 | - if (event.event === MoreActionEvent.DELETE) { | |
188 | - createConfirm({ | |
189 | - iconType: 'warning', | |
190 | - content: '是否确认删除?', | |
191 | - onOk: () => handleDelete(id), | |
192 | - }); | |
193 | - } | |
194 | - if (event.event === MoreActionEvent.EDIT) handleUpdate(id); | |
195 | - if (event.event === MoreActionEvent.COPY) handleCopy(id); | |
196 | - }; | |
197 | - | |
198 | - const handleOpenCreatePanel = () => { | |
199 | - openModal(true, { isEdit: false }); | |
200 | - }; | |
201 | - | |
202 | - const getLayoutInfo = () => { | |
203 | - return unref(dataBoardList).map((item) => { | |
204 | - return { | |
205 | - id: item.i, | |
206 | - h: item.h, | |
207 | - w: item.w, | |
208 | - x: item.x, | |
209 | - y: item.y, | |
210 | - } as Layout; | |
211 | - }); | |
212 | - }; | |
213 | - | |
214 | - const handleSaveLayoutInfo = async () => { | |
215 | - try { | |
216 | - await updateDataBoardLayout({ | |
217 | - boardId: unref(getBoardId), | |
218 | - layout: getLayoutInfo(), | |
219 | - }); | |
220 | - } catch (error) {} | |
221 | - }; | |
222 | - | |
223 | - const { beginSendMessage } = useSocketConnect(dataBoardList); | |
224 | - | |
225 | - const getBasePageComponentData = async () => { | |
226 | - try { | |
227 | - return await getDataComponent(unref(getBoardId)); | |
228 | - } catch (error) {} | |
229 | - return {} as ComponentInfoDetail; | |
230 | - }; | |
231 | - | |
232 | - const getDataBoradDetail = async () => { | |
233 | - try { | |
234 | - return unref(getIsSharePage) ? unref(getSharePageData) : await getBasePageComponentData(); | |
235 | - } catch (error) { | |
236 | - return {} as ComponentInfoDetail; | |
237 | - } | |
238 | - }; | |
239 | - | |
240 | - const loading = ref(false); | |
241 | - const getDataBoardComponent = async () => { | |
242 | - try { | |
243 | - dataBoardList.value = []; | |
244 | - loading.value = true; | |
245 | - const data = await getDataBoradDetail(); | |
246 | - | |
247 | - if (!data.data.componentData) { | |
248 | - dataBoardList.value = []; | |
249 | - return; | |
250 | - } | |
251 | - dataBoardList.value = data.data.componentData.map((item) => { | |
252 | - const index = data.data.componentLayout.findIndex((each) => item.id === each.id); | |
253 | - let layout; | |
254 | - if (!~index) { | |
255 | - layout = {}; | |
256 | - } else { | |
257 | - layout = data.data.componentLayout[index]; | |
258 | - } | |
259 | - return { | |
260 | - i: item.id, | |
261 | - w: layout.w || DEFAULT_WIDGET_WIDTH, | |
262 | - h: layout.h || DEFAULT_WIDGET_HEIGHT, | |
263 | - x: layout.x || 0, | |
264 | - y: layout.y || 0, | |
265 | - record: item, | |
266 | - }; | |
267 | - }); | |
268 | - beginSendMessage(); | |
269 | - } catch (error) { | |
270 | - throw error; | |
271 | - } finally { | |
272 | - loading.value = false; | |
273 | - } | |
274 | - }; | |
275 | - | |
276 | - const handleUpdateComponent = async (id: string) => { | |
277 | - try { | |
278 | - loading.value = true; | |
279 | - const data = await getDataBoradDetail(); | |
280 | - const updateIndex = data.data.componentData.findIndex((item) => item.id === id); | |
281 | - const originalIndex = unref(dataBoardList).findIndex((item) => item.i === id); | |
282 | - | |
283 | - const newUpdateData = data.data.componentData[updateIndex]; | |
284 | - const originalData = unref(dataBoardList)[originalIndex]; | |
285 | - dataBoardList.value[originalIndex] = { | |
286 | - i: id, | |
287 | - w: originalData.w || DEFAULT_WIDGET_WIDTH, | |
288 | - h: originalData.h || DEFAULT_WIDGET_HEIGHT, | |
289 | - x: originalData.x || 0, | |
290 | - y: originalData.y || 0, | |
291 | - width: originalData.width, | |
292 | - height: originalData.height, | |
293 | - record: newUpdateData, | |
294 | - }; | |
295 | - | |
296 | - updateSize(id, 0, 0, originalData.height || 0, originalData.width || 0); | |
297 | - | |
298 | - beginSendMessage(); | |
299 | - } catch (error) { | |
300 | - } finally { | |
301 | - loading.value = false; | |
302 | - } | |
303 | - }; | |
304 | - | |
305 | - const getComponent = (record: DataComponentRecord) => { | |
306 | - const frontComponent = record.frontId; | |
307 | - const component = frontComponentMap.get(frontComponent as FrontComponent); | |
308 | - return component?.Component; | |
309 | - }; | |
310 | - | |
311 | - const getComponentConfig = ( | |
312 | - record: DataBoardLayoutInfo['record'], | |
313 | - dataSourceRecord: DataSource | DataSource[] | |
314 | - ) => { | |
315 | - const frontComponent = record.frontId; | |
316 | - const component = frontComponentMap.get(frontComponent as FrontComponent); | |
317 | - return component?.transformConfig(component.ComponentConfig || {}, record, dataSourceRecord); | |
318 | - }; | |
319 | - | |
320 | - const handleUpdate = async (id: string) => { | |
321 | - const record = unref(dataBoardList).find((item) => item.i === id); | |
322 | - openModal(true, { isEdit: true, record }); | |
323 | - }; | |
324 | - | |
325 | - const { calcLayoutInfo } = useCalcGridLayout(); | |
326 | - | |
327 | - const handleCopy = async (id: string) => { | |
328 | - const record = unref(dataBoardList).find((item) => item.i === id); | |
329 | - try { | |
330 | - const data = await addDataComponent({ | |
331 | - boardId: unref(getBoardId), | |
332 | - record: { | |
333 | - dataBoardId: unref(getBoardId), | |
334 | - frontId: record?.record.frontId, | |
335 | - name: record?.record.name, | |
336 | - remark: record?.record.remark, | |
337 | - dataSource: record?.record.dataSource, | |
338 | - }, | |
339 | - }); | |
340 | - createMessage.success('复制成功'); | |
341 | - const _id = data.data.id; | |
342 | - const layoutInfo = getLayoutInfo(); | |
343 | - | |
344 | - const newGridLayout = calcLayoutInfo(unref(dataBoardList), { | |
345 | - width: record?.w || DEFAULT_WIDGET_HEIGHT, | |
346 | - height: record?.h || DEFAULT_WIDGET_HEIGHT, | |
347 | - }); | |
348 | - layoutInfo.push({ | |
349 | - id: _id, | |
350 | - ...newGridLayout, | |
351 | - } as Layout); | |
352 | - | |
353 | - await updateDataBoardLayout({ | |
354 | - boardId: unref(getBoardId), | |
355 | - layout: layoutInfo, | |
356 | - }); | |
357 | - | |
358 | - await getDataBoardComponent(); | |
359 | - } catch (error) {} | |
360 | - }; | |
361 | - | |
362 | - const handleDelete = async (id: string) => { | |
363 | - try { | |
364 | - const dataBoardId = unref(dataBoardList).find((item) => item.i == id)?.record.dataBoardId; | |
365 | - if (!dataBoardId) return; | |
366 | - await deleteDataComponent({ dataBoardId, ids: [id] }); | |
367 | - createMessage.success('删除成功'); | |
368 | - await getDataBoardComponent(); | |
369 | - } catch (error) {} | |
370 | - }; | |
371 | - | |
372 | - const [registerHistoryDataModal, historyDataModalMethod] = useModal(); | |
373 | - | |
374 | - const handleOpenHistroyDataModal = (record: DataSource[]) => { | |
375 | - historyDataModalMethod.openModal(true, record); | |
376 | - }; | |
377 | - | |
378 | - const hasHistoryTrend = (item: DataBoardLayoutInfo) => { | |
379 | - return frontComponentMap.get(item.record.frontId as FrontComponent)?.hasHistoryTrend; | |
380 | - }; | |
381 | - | |
382 | - onMounted(async () => { | |
383 | - injectBaiDuMapLib(); | |
384 | - injectBaiDuMapTrackAniMationLib(); | |
385 | - getDataBoardComponent(); | |
386 | - }); | |
2 | + import { Palette } from '/@/views/visual/palette'; | |
387 | 3 | </script> |
388 | - | |
389 | 4 | <template> |
390 | - <section class="flex flex-col overflow-hidden h-full w-full board-detail"> | |
391 | - <PageHeader v-if="!getIsSharePage"> | |
392 | - <template #title> | |
393 | - <div class="flex items-center"> | |
394 | - <img | |
395 | - :src="getAceClass === 'dark' ? backWhiteIcon : backIcon" | |
396 | - v-if="!getIsSharePage" | |
397 | - class="mr-3 cursor-pointer" | |
398 | - @click="handleBack" | |
399 | - /> | |
400 | - <span class="text-lg" color="#333">{{ getDataBoardName }}</span> | |
401 | - </div> | |
402 | - </template> | |
403 | - <template #extra> | |
404 | - <Authority :value="VisualComponentPermission.CREATE"> | |
405 | - <Button | |
406 | - v-if="!getIsSharePage && !isCustomerUser" | |
407 | - type="primary" | |
408 | - @click="handleOpenCreatePanel" | |
409 | - > | |
410 | - 创建组件 | |
411 | - </Button> | |
412 | - </Authority> | |
413 | - </template> | |
414 | - <div> | |
415 | - <span class="mr-3 text-sm" style="color: #666">已创建组件:</span> | |
416 | - <span style="color: #409eff"> {{ dataBoardList.length }}个</span> | |
417 | - </div> | |
418 | - </PageHeader> | |
419 | - <section class="flex-1"> | |
420 | - <Spin :spinning="loading"> | |
421 | - <GridLayout | |
422 | - v-model:layout="dataBoardList" | |
423 | - :col-num="GirdLayoutColNum" | |
424 | - :row-height="30" | |
425 | - :margin="[GridLayoutMargin, GridLayoutMargin]" | |
426 | - :is-draggable="draggable" | |
427 | - :is-resizable="resizable" | |
428 | - :vertical-compact="true" | |
429 | - :use-css-transforms="true" | |
430 | - style="width: 100%" | |
431 | - > | |
432 | - <GridItem | |
433 | - v-for="item in dataBoardList" | |
434 | - :key="item.i" | |
435 | - :static="item.static" | |
436 | - :x="item.x" | |
437 | - :y="item.y" | |
438 | - :w="item.w" | |
439 | - :h="item.h" | |
440 | - :i="item.i" | |
441 | - :min-h="DEFAULT_MIN_HEIGHT" | |
442 | - :min-w="DEFAULT_MIN_WIDTH" | |
443 | - :style="{ display: 'flex', flexWrap: 'wrap' }" | |
444 | - class="grid-item-layout" | |
445 | - @resized="itemResized" | |
446 | - @resize="itemResize" | |
447 | - @moved="itemMoved" | |
448 | - @container-resized="itemContainerResized" | |
449 | - drag-ignore-from=".no-drag" | |
450 | - > | |
451 | - <WidgetWrapper | |
452 | - :key="item.i" | |
453 | - :ref="(el: Element) => setComponentRef(el, item)" | |
454 | - :record="item.record" | |
455 | - :data-source="item.record.dataSource" | |
456 | - > | |
457 | - <template #header> | |
458 | - <BaseWidgetHeader | |
459 | - :record="item.record.dataSource" | |
460 | - :id="item.record.id" | |
461 | - :panel-name="item.record.name" | |
462 | - @action="handleMoreAction" | |
463 | - > | |
464 | - <template #moreAction> | |
465 | - <Tooltip v-if="!isCustomerUser" title="趋势"> | |
466 | - <img | |
467 | - :src="trendIcon" | |
468 | - v-if="!getIsSharePage && hasHistoryTrend(item)" | |
469 | - class="cursor-pointer w-4.5 h-4.5" | |
470 | - @click="handleOpenHistroyDataModal(item.record.dataSource)" | |
471 | - /> | |
472 | - </Tooltip> | |
473 | - </template> | |
474 | - </BaseWidgetHeader> | |
475 | - </template> | |
476 | - <template #controls="{ record, add, remove, update }"> | |
477 | - <component | |
478 | - :is="getComponent(item.record)" | |
479 | - :add="add" | |
480 | - :remove="remove" | |
481 | - :update="update" | |
482 | - :radio="record.radio || {}" | |
483 | - v-bind="getComponentConfig(item.record, record)" | |
484 | - :random="false" | |
485 | - /> | |
486 | - </template> | |
487 | - </WidgetWrapper> | |
488 | - </GridItem> | |
489 | - </GridLayout> | |
490 | - <Empty | |
491 | - v-if="!dataBoardList.length" | |
492 | - class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" | |
493 | - /> | |
494 | - </Spin> | |
495 | - </section> | |
496 | - <DataBindModal | |
497 | - :layout="dataBoardList" | |
498 | - @register="register" | |
499 | - @update="handleUpdateComponent" | |
500 | - @create="getDataBoardComponent" | |
501 | - /> | |
502 | - <HistoryTrendModal @register="registerHistoryDataModal" /> | |
503 | - </section> | |
5 | + <Palette /> | |
504 | 6 | </template> |
505 | - | |
506 | -<style lang="less" scoped> | |
507 | - .vue-grid-item:not(.vue-grid-placeholder) { | |
508 | - background: #fff; | |
509 | - border: none !important; | |
510 | - | |
511 | - /* border: 1px solid black; */ | |
512 | - } | |
513 | - | |
514 | - .vue-grid-item .resizing { | |
515 | - opacity: 0.9; | |
516 | - } | |
517 | - | |
518 | - .vue-grid-item .static { | |
519 | - background: #cce; | |
520 | - } | |
521 | - | |
522 | - .vue-grid-item .text { | |
523 | - font-size: 24px; | |
524 | - text-align: center; | |
525 | - position: absolute; | |
526 | - top: 0; | |
527 | - bottom: 0; | |
528 | - left: 0; | |
529 | - right: 0; | |
530 | - margin: auto; | |
531 | - height: 100%; | |
532 | - width: 100%; | |
533 | - } | |
534 | - | |
535 | - .vue-grid-item .no-drag { | |
536 | - height: 100%; | |
537 | - width: 100%; | |
538 | - } | |
539 | - | |
540 | - .vue-grid-item .minMax { | |
541 | - font-size: 12px; | |
542 | - } | |
543 | - | |
544 | - .vue-grid-item .add { | |
545 | - cursor: pointer; | |
546 | - } | |
547 | - | |
548 | - .vue-draggable-handle { | |
549 | - position: absolute; | |
550 | - width: 20px; | |
551 | - height: 20px; | |
552 | - top: 0; | |
553 | - left: 0; | |
554 | - background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><circle cx='5' cy='5' r='5' fill='#999999'/></svg>") | |
555 | - no-repeat; | |
556 | - background-position: bottom right; | |
557 | - padding: 0 8px 8px 0; | |
558 | - background-repeat: no-repeat; | |
559 | - background-origin: content-box; | |
560 | - box-sizing: border-box; | |
561 | - cursor: pointer; | |
562 | - } | |
563 | - | |
564 | - .grid-item-layout { | |
565 | - overflow: hidden; | |
566 | - border: 1px solid #eee !important; | |
567 | - background-color: #fcfcfc !important; | |
568 | - } | |
569 | - | |
570 | - .board-detail:deep(.ant-page-header) { | |
571 | - padding: 20px 20px 0 20px; | |
572 | - } | |
573 | - | |
574 | - .board-detail:deep(.ant-page-header-heading) { | |
575 | - height: 78px; | |
576 | - padding: 0 20px 0 20px; | |
577 | - box-sizing: border-box; | |
578 | - background-color: #fff; | |
579 | - } | |
580 | - | |
581 | - [data-theme='dark'] .board-detail:deep(.ant-page-header-heading) { | |
582 | - @apply bg-dark-900; | |
583 | - } | |
584 | - | |
585 | - .board-detail:deep(.ant-page-header-heading-extra) { | |
586 | - margin: 0; | |
587 | - line-height: 78px; | |
588 | - } | |
589 | - | |
590 | - .board-detail:deep(.ant-page-header-content) { | |
591 | - padding-top: 20px; | |
592 | - } | |
593 | - | |
594 | - :deep(.vue-resizable-handle) { | |
595 | - z-index: 99; | |
596 | - } | |
597 | -</style> | ... | ... |
src/views/visual/board/detail/old-index.vue
0 → 100644
1 | +<script lang="ts" setup> | |
2 | + import { Button, PageHeader, Empty, Spin, Tooltip } from 'ant-design-vue'; | |
3 | + import { GridItem, GridLayout } from 'vue3-grid-layout'; | |
4 | + import { nextTick, onMounted, ref } from 'vue'; | |
5 | + import WidgetWrapper from '../components/WidgetWrapper/WidgetWrapper.vue'; | |
6 | + import BaseWidgetHeader from '../components/WidgetHeader/BaseWidgetHeader.vue'; | |
7 | + import { DropMenu } from '/@/components/Dropdown'; | |
8 | + import DataBindModal from './components/DataBindModal.vue'; | |
9 | + import { useModal } from '/@/components/Modal'; | |
10 | + import { | |
11 | + decode, | |
12 | + DEFAULT_MAX_COL, | |
13 | + DEFAULT_MIN_HEIGHT, | |
14 | + DEFAULT_MIN_WIDTH, | |
15 | + DEFAULT_WIDGET_HEIGHT, | |
16 | + DEFAULT_WIDGET_WIDTH, | |
17 | + MoreActionEvent, | |
18 | + VisualComponentPermission, | |
19 | + } from '../config/config'; | |
20 | + import { | |
21 | + addDataComponent, | |
22 | + deleteDataComponent, | |
23 | + getDataComponent, | |
24 | + updateDataBoardLayout, | |
25 | + } from '/@/api/dataBoard'; | |
26 | + import { useRoute, useRouter } from 'vue-router'; | |
27 | + import { computed, unref } from '@vue/reactivity'; | |
28 | + import { | |
29 | + ComponentInfoDetail, | |
30 | + DataComponentRecord, | |
31 | + DataSource, | |
32 | + Layout, | |
33 | + } from '/@/api/dataBoard/model'; | |
34 | + import { frontComponentMap } from '../components/help'; | |
35 | + import { calcScale } from './config/util'; | |
36 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
37 | + import { DataBoardLayoutInfo } from '../types/type'; | |
38 | + import Authority from '/@/components/Authority/src/Authority.vue'; | |
39 | + import { useSocketConnect } from '../hook/useSocketConnect'; | |
40 | + import { buildUUID } from '/@/utils/uuid'; | |
41 | + import HistoryTrendModal from './components/HistoryTrendModal.vue'; | |
42 | + import trendIcon from '/@/assets/svg/trend.svg'; | |
43 | + import backIcon from '/@/assets/images/back.png'; | |
44 | + import backWhiteIcon from '/@/assets/images/backWhite.png'; | |
45 | + import { useCalcGridLayout } from '../hook/useCalcGridLayout'; | |
46 | + import { FrontComponent } from '../const/const'; | |
47 | + import { useScript } from '/@/hooks/web/useScript'; | |
48 | + import { BAI_DU_MAP_GL_LIB, BAI_DU_MAP_TRACK_ANIMATION } from '/@/utils/fnUtils'; | |
49 | + import { useAppStore } from '/@/store/modules/app'; | |
50 | + import { useRole } from '/@/hooks/business/useRole'; | |
51 | + | |
52 | + const userStore = useAppStore(); | |
53 | + | |
54 | + const getAceClass = computed((): string => userStore.getDarkMode); | |
55 | + | |
56 | + const props = defineProps<{ | |
57 | + value?: Recordable; | |
58 | + }>(); | |
59 | + | |
60 | + const ROUTE = useRoute(); | |
61 | + | |
62 | + const ROUTER = useRouter(); | |
63 | + | |
64 | + const { isCustomerUser } = useRole(); | |
65 | + const { toPromise: injectBaiDuMapLib } = useScript({ src: BAI_DU_MAP_GL_LIB }); | |
66 | + const { toPromise: injectBaiDuMapTrackAniMationLib } = useScript({ | |
67 | + src: BAI_DU_MAP_TRACK_ANIMATION, | |
68 | + }); | |
69 | + | |
70 | + const { createMessage, createConfirm } = useMessage(); | |
71 | + | |
72 | + const getBoardId = computed(() => { | |
73 | + return decode((ROUTE.params as { boardId: string }).boardId); | |
74 | + }); | |
75 | + | |
76 | + const getDataBoardName = computed(() => { | |
77 | + return decode((ROUTE.params as { boardName: string }).boardName || ''); | |
78 | + }); | |
79 | + | |
80 | + const getSharePageData = computed(() => { | |
81 | + return props.value!; | |
82 | + }); | |
83 | + | |
84 | + const getIsSharePage = computed(() => { | |
85 | + return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId'); | |
86 | + }); | |
87 | + | |
88 | + const widgetEl = new Map<string, Fn>(); | |
89 | + | |
90 | + const dataBoardList = ref<DataBoardLayoutInfo[]>([]); | |
91 | + | |
92 | + const draggable = ref(!unref(getIsSharePage)); | |
93 | + const resizable = ref(!unref(getIsSharePage)); | |
94 | + | |
95 | + const GirdLayoutColNum = DEFAULT_MAX_COL; | |
96 | + const GridLayoutMargin = 20; | |
97 | + | |
98 | + const handleBack = () => { | |
99 | + if (unref(getIsSharePage)) return; | |
100 | + ROUTER.go(-1); | |
101 | + }; | |
102 | + | |
103 | + function updateSize(i: string, _newH: number, _newW: number, newHPx: number, newWPx: number) { | |
104 | + newWPx = Number(newWPx); | |
105 | + newHPx = Number(newHPx); | |
106 | + | |
107 | + const data = dataBoardList.value.find((item) => item.i === i)!; | |
108 | + const length = data.record.dataSource.length || 0; | |
109 | + | |
110 | + const row = Math.floor(Math.pow(length, 0.5)); | |
111 | + const col = Math.floor(length / row); | |
112 | + let width = Math.floor(100 / col); | |
113 | + let height = Math.floor(100 / row); | |
114 | + | |
115 | + const WHRatio = newWPx / newHPx; | |
116 | + const HWRatio = newHPx / newWPx; | |
117 | + | |
118 | + if (WHRatio > 1.6) { | |
119 | + width = Math.floor(100 / length); | |
120 | + height = 100; | |
121 | + } | |
122 | + | |
123 | + if (HWRatio > 1.6) { | |
124 | + height = Math.floor(100 / length); | |
125 | + width = 100; | |
126 | + } | |
127 | + | |
128 | + data.width = newWPx; | |
129 | + data.height = newHPx; | |
130 | + | |
131 | + data.record.dataSource = data.record.dataSource.map((item) => { | |
132 | + if (!item.uuid) item.uuid = buildUUID(); | |
133 | + return { | |
134 | + ...item, | |
135 | + width, | |
136 | + height, | |
137 | + radio: calcScale(newWPx, newHPx, width, height), | |
138 | + }; | |
139 | + }); | |
140 | + | |
141 | + nextTick(() => { | |
142 | + const updateFn = widgetEl.get(i); | |
143 | + if (updateFn) updateFn(); | |
144 | + }); | |
145 | + } | |
146 | + | |
147 | + const itemResize = (i: string, newH: number, newW: number, newHPx: number, newWPx: number) => { | |
148 | + updateSize(i, newH, newW, newHPx, newWPx); | |
149 | + }; | |
150 | + | |
151 | + const itemResized = (i: string, newH: number, newW: number, newHPx: number, newWPx: number) => { | |
152 | + handleSaveLayoutInfo(); | |
153 | + updateSize(i, newH, newW, newHPx, newWPx); | |
154 | + }; | |
155 | + | |
156 | + const itemContainerResized = ( | |
157 | + i: string, | |
158 | + newH: number, | |
159 | + newW: number, | |
160 | + newHPx: number, | |
161 | + newWPx: number | |
162 | + ) => { | |
163 | + updateSize(i, newH, newW, newHPx, newWPx); | |
164 | + }; | |
165 | + | |
166 | + const itemMoved = (i: string) => { | |
167 | + handleSaveLayoutInfo(); | |
168 | + updateCharts(i); | |
169 | + }; | |
170 | + | |
171 | + const updateCharts = (i: string) => { | |
172 | + nextTick(() => { | |
173 | + const updateFn = widgetEl.get(i); | |
174 | + if (updateFn) updateFn(); | |
175 | + }); | |
176 | + }; | |
177 | + | |
178 | + const setComponentRef = (el: Element, record: DataBoardLayoutInfo) => { | |
179 | + if (widgetEl.has(record.i)) widgetEl.delete(record.i); | |
180 | + if (el && (el as unknown as { update: Fn }).update) | |
181 | + widgetEl.set(record.i, (el as unknown as { update: Fn }).update); | |
182 | + }; | |
183 | + | |
184 | + const [register, { openModal }] = useModal(); | |
185 | + | |
186 | + const handleMoreAction = (event: DropMenu, id: string) => { | |
187 | + if (event.event === MoreActionEvent.DELETE) { | |
188 | + createConfirm({ | |
189 | + iconType: 'warning', | |
190 | + content: '是否确认删除?', | |
191 | + onOk: () => handleDelete(id), | |
192 | + }); | |
193 | + } | |
194 | + if (event.event === MoreActionEvent.EDIT) handleUpdate(id); | |
195 | + if (event.event === MoreActionEvent.COPY) handleCopy(id); | |
196 | + }; | |
197 | + | |
198 | + const handleOpenCreatePanel = () => { | |
199 | + openModal(true, { isEdit: false }); | |
200 | + }; | |
201 | + | |
202 | + const getLayoutInfo = () => { | |
203 | + return unref(dataBoardList).map((item) => { | |
204 | + return { | |
205 | + id: item.i, | |
206 | + h: item.h, | |
207 | + w: item.w, | |
208 | + x: item.x, | |
209 | + y: item.y, | |
210 | + } as Layout; | |
211 | + }); | |
212 | + }; | |
213 | + | |
214 | + const handleSaveLayoutInfo = async () => { | |
215 | + try { | |
216 | + await updateDataBoardLayout({ | |
217 | + boardId: unref(getBoardId), | |
218 | + layout: getLayoutInfo(), | |
219 | + }); | |
220 | + } catch (error) {} | |
221 | + }; | |
222 | + | |
223 | + const { beginSendMessage } = useSocketConnect(dataBoardList); | |
224 | + | |
225 | + const getBasePageComponentData = async () => { | |
226 | + try { | |
227 | + return await getDataComponent(unref(getBoardId)); | |
228 | + } catch (error) {} | |
229 | + return {} as ComponentInfoDetail; | |
230 | + }; | |
231 | + | |
232 | + const getDataBoradDetail = async () => { | |
233 | + try { | |
234 | + return unref(getIsSharePage) ? unref(getSharePageData) : await getBasePageComponentData(); | |
235 | + } catch (error) { | |
236 | + return {} as ComponentInfoDetail; | |
237 | + } | |
238 | + }; | |
239 | + | |
240 | + const loading = ref(false); | |
241 | + const getDataBoardComponent = async () => { | |
242 | + try { | |
243 | + dataBoardList.value = []; | |
244 | + loading.value = true; | |
245 | + const data = await getDataBoradDetail(); | |
246 | + | |
247 | + if (!data.data.componentData) { | |
248 | + dataBoardList.value = []; | |
249 | + return; | |
250 | + } | |
251 | + dataBoardList.value = data.data.componentData.map((item) => { | |
252 | + const index = data.data.componentLayout.findIndex((each) => item.id === each.id); | |
253 | + let layout; | |
254 | + if (!~index) { | |
255 | + layout = {}; | |
256 | + } else { | |
257 | + layout = data.data.componentLayout[index]; | |
258 | + } | |
259 | + return { | |
260 | + i: item.id, | |
261 | + w: layout.w || DEFAULT_WIDGET_WIDTH, | |
262 | + h: layout.h || DEFAULT_WIDGET_HEIGHT, | |
263 | + x: layout.x || 0, | |
264 | + y: layout.y || 0, | |
265 | + record: item, | |
266 | + }; | |
267 | + }); | |
268 | + beginSendMessage(); | |
269 | + } catch (error) { | |
270 | + throw error; | |
271 | + } finally { | |
272 | + loading.value = false; | |
273 | + } | |
274 | + }; | |
275 | + | |
276 | + const handleUpdateComponent = async (id: string) => { | |
277 | + try { | |
278 | + loading.value = true; | |
279 | + const data = await getDataBoradDetail(); | |
280 | + const updateIndex = data.data.componentData.findIndex((item) => item.id === id); | |
281 | + const originalIndex = unref(dataBoardList).findIndex((item) => item.i === id); | |
282 | + | |
283 | + const newUpdateData = data.data.componentData[updateIndex]; | |
284 | + const originalData = unref(dataBoardList)[originalIndex]; | |
285 | + dataBoardList.value[originalIndex] = { | |
286 | + i: id, | |
287 | + w: originalData.w || DEFAULT_WIDGET_WIDTH, | |
288 | + h: originalData.h || DEFAULT_WIDGET_HEIGHT, | |
289 | + x: originalData.x || 0, | |
290 | + y: originalData.y || 0, | |
291 | + width: originalData.width, | |
292 | + height: originalData.height, | |
293 | + record: newUpdateData, | |
294 | + }; | |
295 | + | |
296 | + updateSize(id, 0, 0, originalData.height || 0, originalData.width || 0); | |
297 | + | |
298 | + beginSendMessage(); | |
299 | + } catch (error) { | |
300 | + } finally { | |
301 | + loading.value = false; | |
302 | + } | |
303 | + }; | |
304 | + | |
305 | + const getComponent = (record: DataComponentRecord) => { | |
306 | + const frontComponent = record.frontId; | |
307 | + const component = frontComponentMap.get(frontComponent as FrontComponent); | |
308 | + return component?.Component; | |
309 | + }; | |
310 | + | |
311 | + const getComponentConfig = ( | |
312 | + record: DataBoardLayoutInfo['record'], | |
313 | + dataSourceRecord: DataSource | DataSource[] | |
314 | + ) => { | |
315 | + const frontComponent = record.frontId; | |
316 | + const component = frontComponentMap.get(frontComponent as FrontComponent); | |
317 | + return component?.transformConfig(component.ComponentConfig || {}, record, dataSourceRecord); | |
318 | + }; | |
319 | + | |
320 | + const handleUpdate = async (id: string) => { | |
321 | + const record = unref(dataBoardList).find((item) => item.i === id); | |
322 | + openModal(true, { isEdit: true, record }); | |
323 | + }; | |
324 | + | |
325 | + const { calcLayoutInfo } = useCalcGridLayout(); | |
326 | + | |
327 | + const handleCopy = async (id: string) => { | |
328 | + const record = unref(dataBoardList).find((item) => item.i === id); | |
329 | + try { | |
330 | + const data = await addDataComponent({ | |
331 | + boardId: unref(getBoardId), | |
332 | + record: { | |
333 | + dataBoardId: unref(getBoardId), | |
334 | + frontId: record?.record.frontId, | |
335 | + name: record?.record.name, | |
336 | + remark: record?.record.remark, | |
337 | + dataSource: record?.record.dataSource, | |
338 | + }, | |
339 | + }); | |
340 | + createMessage.success('复制成功'); | |
341 | + const _id = data.data.id; | |
342 | + const layoutInfo = getLayoutInfo(); | |
343 | + | |
344 | + const newGridLayout = calcLayoutInfo(unref(dataBoardList), { | |
345 | + width: record?.w || DEFAULT_WIDGET_HEIGHT, | |
346 | + height: record?.h || DEFAULT_WIDGET_HEIGHT, | |
347 | + }); | |
348 | + layoutInfo.push({ | |
349 | + id: _id, | |
350 | + ...newGridLayout, | |
351 | + } as Layout); | |
352 | + | |
353 | + await updateDataBoardLayout({ | |
354 | + boardId: unref(getBoardId), | |
355 | + layout: layoutInfo, | |
356 | + }); | |
357 | + | |
358 | + await getDataBoardComponent(); | |
359 | + } catch (error) {} | |
360 | + }; | |
361 | + | |
362 | + const handleDelete = async (id: string) => { | |
363 | + try { | |
364 | + const dataBoardId = unref(dataBoardList).find((item) => item.i == id)?.record.dataBoardId; | |
365 | + if (!dataBoardId) return; | |
366 | + await deleteDataComponent({ dataBoardId, ids: [id] }); | |
367 | + createMessage.success('删除成功'); | |
368 | + await getDataBoardComponent(); | |
369 | + } catch (error) {} | |
370 | + }; | |
371 | + | |
372 | + const [registerHistoryDataModal, historyDataModalMethod] = useModal(); | |
373 | + | |
374 | + const handleOpenHistroyDataModal = (record: DataSource[]) => { | |
375 | + historyDataModalMethod.openModal(true, record); | |
376 | + }; | |
377 | + | |
378 | + const hasHistoryTrend = (item: DataBoardLayoutInfo) => { | |
379 | + return frontComponentMap.get(item.record.frontId as FrontComponent)?.hasHistoryTrend; | |
380 | + }; | |
381 | + | |
382 | + onMounted(async () => { | |
383 | + injectBaiDuMapLib(); | |
384 | + injectBaiDuMapTrackAniMationLib(); | |
385 | + getDataBoardComponent(); | |
386 | + }); | |
387 | +</script> | |
388 | + | |
389 | +<template> | |
390 | + <section class="flex flex-col overflow-hidden h-full w-full board-detail"> | |
391 | + <PageHeader v-if="!getIsSharePage"> | |
392 | + <template #title> | |
393 | + <div class="flex items-center"> | |
394 | + <img | |
395 | + :src="getAceClass === 'dark' ? backWhiteIcon : backIcon" | |
396 | + v-if="!getIsSharePage" | |
397 | + class="mr-3 cursor-pointer" | |
398 | + @click="handleBack" | |
399 | + /> | |
400 | + <span class="text-lg" color="#333">{{ getDataBoardName }}</span> | |
401 | + </div> | |
402 | + </template> | |
403 | + <template #extra> | |
404 | + <Authority :value="VisualComponentPermission.CREATE"> | |
405 | + <Button | |
406 | + v-if="!getIsSharePage && !isCustomerUser" | |
407 | + type="primary" | |
408 | + @click="handleOpenCreatePanel" | |
409 | + > | |
410 | + 创建组件 | |
411 | + </Button> | |
412 | + </Authority> | |
413 | + </template> | |
414 | + <div> | |
415 | + <span class="mr-3 text-sm" style="color: #666">已创建组件:</span> | |
416 | + <span style="color: #409eff"> {{ dataBoardList.length }}个</span> | |
417 | + </div> | |
418 | + </PageHeader> | |
419 | + <section class="flex-1"> | |
420 | + <Spin :spinning="loading"> | |
421 | + <GridLayout | |
422 | + v-model:layout="dataBoardList" | |
423 | + :col-num="GirdLayoutColNum" | |
424 | + :row-height="30" | |
425 | + :margin="[GridLayoutMargin, GridLayoutMargin]" | |
426 | + :is-draggable="draggable" | |
427 | + :is-resizable="resizable" | |
428 | + :vertical-compact="true" | |
429 | + :use-css-transforms="true" | |
430 | + style="width: 100%" | |
431 | + > | |
432 | + <GridItem | |
433 | + v-for="item in dataBoardList" | |
434 | + :key="item.i" | |
435 | + :static="item.static" | |
436 | + :x="item.x" | |
437 | + :y="item.y" | |
438 | + :w="item.w" | |
439 | + :h="item.h" | |
440 | + :i="item.i" | |
441 | + :min-h="DEFAULT_MIN_HEIGHT" | |
442 | + :min-w="DEFAULT_MIN_WIDTH" | |
443 | + :style="{ display: 'flex', flexWrap: 'wrap' }" | |
444 | + class="grid-item-layout" | |
445 | + @resized="itemResized" | |
446 | + @resize="itemResize" | |
447 | + @moved="itemMoved" | |
448 | + @container-resized="itemContainerResized" | |
449 | + drag-ignore-from=".no-drag" | |
450 | + > | |
451 | + <WidgetWrapper | |
452 | + :key="item.i" | |
453 | + :ref="(el: Element) => setComponentRef(el, item)" | |
454 | + :record="item.record" | |
455 | + :data-source="item.record.dataSource" | |
456 | + > | |
457 | + <template #header> | |
458 | + <BaseWidgetHeader | |
459 | + :record="item.record.dataSource" | |
460 | + :id="item.record.id" | |
461 | + :panel-name="item.record.name" | |
462 | + @action="handleMoreAction" | |
463 | + > | |
464 | + <template #moreAction> | |
465 | + <Tooltip v-if="!isCustomerUser" title="趋势"> | |
466 | + <img | |
467 | + :src="trendIcon" | |
468 | + v-if="!getIsSharePage && hasHistoryTrend(item)" | |
469 | + class="cursor-pointer w-4.5 h-4.5" | |
470 | + @click="handleOpenHistroyDataModal(item.record.dataSource)" | |
471 | + /> | |
472 | + </Tooltip> | |
473 | + </template> | |
474 | + </BaseWidgetHeader> | |
475 | + </template> | |
476 | + <template #controls="{ record, add, remove, update }"> | |
477 | + <component | |
478 | + :is="getComponent(item.record)" | |
479 | + :add="add" | |
480 | + :remove="remove" | |
481 | + :update="update" | |
482 | + :radio="record.radio || {}" | |
483 | + v-bind="getComponentConfig(item.record, record)" | |
484 | + :random="false" | |
485 | + /> | |
486 | + </template> | |
487 | + </WidgetWrapper> | |
488 | + </GridItem> | |
489 | + </GridLayout> | |
490 | + <Empty | |
491 | + v-if="!dataBoardList.length" | |
492 | + class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" | |
493 | + /> | |
494 | + </Spin> | |
495 | + </section> | |
496 | + <DataBindModal | |
497 | + :layout="dataBoardList" | |
498 | + @register="register" | |
499 | + @update="handleUpdateComponent" | |
500 | + @create="getDataBoardComponent" | |
501 | + /> | |
502 | + <HistoryTrendModal @register="registerHistoryDataModal" /> | |
503 | + </section> | |
504 | +</template> | |
505 | + | |
506 | +<style lang="less" scoped> | |
507 | + .vue-grid-item:not(.vue-grid-placeholder) { | |
508 | + background: #fff; | |
509 | + border: none !important; | |
510 | + | |
511 | + /* border: 1px solid black; */ | |
512 | + } | |
513 | + | |
514 | + .vue-grid-item .resizing { | |
515 | + opacity: 0.9; | |
516 | + } | |
517 | + | |
518 | + .vue-grid-item .static { | |
519 | + background: #cce; | |
520 | + } | |
521 | + | |
522 | + .vue-grid-item .text { | |
523 | + font-size: 24px; | |
524 | + text-align: center; | |
525 | + position: absolute; | |
526 | + top: 0; | |
527 | + bottom: 0; | |
528 | + left: 0; | |
529 | + right: 0; | |
530 | + margin: auto; | |
531 | + height: 100%; | |
532 | + width: 100%; | |
533 | + } | |
534 | + | |
535 | + .vue-grid-item .no-drag { | |
536 | + height: 100%; | |
537 | + width: 100%; | |
538 | + } | |
539 | + | |
540 | + .vue-grid-item .minMax { | |
541 | + font-size: 12px; | |
542 | + } | |
543 | + | |
544 | + .vue-grid-item .add { | |
545 | + cursor: pointer; | |
546 | + } | |
547 | + | |
548 | + .vue-draggable-handle { | |
549 | + position: absolute; | |
550 | + width: 20px; | |
551 | + height: 20px; | |
552 | + top: 0; | |
553 | + left: 0; | |
554 | + background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><circle cx='5' cy='5' r='5' fill='#999999'/></svg>") | |
555 | + no-repeat; | |
556 | + background-position: bottom right; | |
557 | + padding: 0 8px 8px 0; | |
558 | + background-repeat: no-repeat; | |
559 | + background-origin: content-box; | |
560 | + box-sizing: border-box; | |
561 | + cursor: pointer; | |
562 | + } | |
563 | + | |
564 | + .grid-item-layout { | |
565 | + overflow: hidden; | |
566 | + border: 1px solid #eee !important; | |
567 | + background-color: #fcfcfc !important; | |
568 | + } | |
569 | + | |
570 | + .board-detail:deep(.ant-page-header) { | |
571 | + padding: 20px 20px 0 20px; | |
572 | + } | |
573 | + | |
574 | + .board-detail:deep(.ant-page-header-heading) { | |
575 | + height: 78px; | |
576 | + padding: 0 20px 0 20px; | |
577 | + box-sizing: border-box; | |
578 | + background-color: #fff; | |
579 | + } | |
580 | + | |
581 | + [data-theme='dark'] .board-detail:deep(.ant-page-header-heading) { | |
582 | + @apply bg-dark-900; | |
583 | + } | |
584 | + | |
585 | + .board-detail:deep(.ant-page-header-heading-extra) { | |
586 | + margin: 0; | |
587 | + line-height: 78px; | |
588 | + } | |
589 | + | |
590 | + .board-detail:deep(.ant-page-header-content) { | |
591 | + padding-top: 20px; | |
592 | + } | |
593 | + | |
594 | + :deep(.vue-resizable-handle) { | |
595 | + z-index: 99; | |
596 | + } | |
597 | +</style> | ... | ... |
1 | +export { default as BasicDataSource } from './index.vue'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { CreateComponentType } from '../../packages/index.type'; | |
3 | + import { BasicForm, useForm } from '/@/components/Form'; | |
4 | + import { dataSourceSchema } from '/@/views/visual/board/detail/config/basicConfiguration'; | |
5 | + import { | |
6 | + PublicComponentValueType, | |
7 | + PublicFormInstaceType, | |
8 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
9 | + | |
10 | + const props = defineProps<{ | |
11 | + values: PublicComponentValueType; | |
12 | + componentConfig: CreateComponentType; | |
13 | + }>(); | |
14 | + | |
15 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | |
16 | + labelWidth: 0, | |
17 | + showActionButtonGroup: false, | |
18 | + layout: 'horizontal', | |
19 | + labelCol: { span: 0 }, | |
20 | + schemas: dataSourceSchema(false, props.componentConfig.componentConfig.key), | |
21 | + }); | |
22 | + | |
23 | + const getFormValues = () => { | |
24 | + return getFieldsValue(); | |
25 | + }; | |
26 | + | |
27 | + const setFormValues = (record: Recordable) => { | |
28 | + return setFieldsValue(record); | |
29 | + }; | |
30 | + | |
31 | + defineExpose({ | |
32 | + getFormValues, | |
33 | + setFormValues, | |
34 | + validate, | |
35 | + resetFormValues: resetFields, | |
36 | + } as PublicFormInstaceType); | |
37 | +</script> | |
38 | + | |
39 | +<template> | |
40 | + <BasicForm @register="register" /> | |
41 | +</template> | ... | ... |
1 | +export { default as DeviceName } from './index.vue'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ComponentPropsConfigType } from '../../packages/index.type'; | |
3 | + | |
4 | + defineProps<{ | |
5 | + config: ComponentPropsConfigType; | |
6 | + }>(); | |
7 | +</script> | |
8 | + | |
9 | +<template> | |
10 | + <div v-if="config.option?.componentInfo?.showDeviceName" class="h-8 font-semibold"> | |
11 | + {{ config.option.deviceRename || config.option.deviceName }} | |
12 | + </div> | |
13 | +</template> | ... | ... |
1 | +export { default as UpdateTime } from './index.vue'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { Tooltip } from 'ant-design-vue'; | |
3 | + import { formatToDateTime } from '/@/utils/dateUtil'; | |
4 | + const props = defineProps<{ | |
5 | + time?: number | null; | |
6 | + }>(); | |
7 | + | |
8 | + const formatDate = (time?: Nullable<number>) => { | |
9 | + return props.time ? formatToDateTime(time, 'YYYY-MM-DD HH:mm:ss') : '暂无更新时间'; | |
10 | + }; | |
11 | +</script> | |
12 | + | |
13 | +<template> | |
14 | + <div class="flex p-5 justify-center items-center text-gray-400 text-xs w-full dark:text-light-50"> | |
15 | + <Tooltip :title="formatDate(time)"> | |
16 | + <div class="truncate px-1"> | |
17 | + <span>更新时间:</span> | |
18 | + <span class="ml-2">{{ formatDate(time) }}</span> | |
19 | + </div> | |
20 | + </Tooltip> | |
21 | + </div> | |
22 | +</template> | ... | ... |
1 | +import { FormSchema } from '/@/components/Form'; | |
2 | + | |
3 | +export type BasicInfoFormValueType = Record<BasicConfigField, string>; | |
4 | + | |
5 | +export enum BasicConfigField { | |
6 | + NAME = 'name', | |
7 | + REMARK = 'remark', | |
8 | +} | |
9 | + | |
10 | +export const basicSchema: FormSchema[] = [ | |
11 | + { | |
12 | + field: BasicConfigField.NAME, | |
13 | + label: '组件名称', | |
14 | + component: 'Input', | |
15 | + componentProps: { | |
16 | + placeholder: '请输入组件名称', | |
17 | + maxLength: 32, | |
18 | + }, | |
19 | + }, | |
20 | + { | |
21 | + field: BasicConfigField.REMARK, | |
22 | + label: '组件备注', | |
23 | + component: 'InputTextArea', | |
24 | + componentProps: { | |
25 | + placeholder: '请输入组件备注', | |
26 | + maxLength: 255, | |
27 | + }, | |
28 | + }, | |
29 | +]; | ... | ... |
1 | +export { default as BasicInfoForm } from './index.vue'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { PublicFormInstaceType } from '../../index.type'; | |
3 | + import { BasicInfoFormValueType, basicSchema } from './config'; | |
4 | + import { BasicForm, useForm } from '/@/components/Form'; | |
5 | + | |
6 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | |
7 | + schemas: basicSchema, | |
8 | + showActionButtonGroup: false, | |
9 | + labelWidth: 96, | |
10 | + }); | |
11 | + | |
12 | + const getFormValues = () => { | |
13 | + return getFieldsValue() as BasicInfoFormValueType; | |
14 | + }; | |
15 | + | |
16 | + const setFormValues = (record: Partial<BasicInfoFormValueType>) => { | |
17 | + setFieldsValue(record); | |
18 | + }; | |
19 | + | |
20 | + defineExpose({ | |
21 | + getFormValues, | |
22 | + setFormValues, | |
23 | + resetFormValues: resetFields, | |
24 | + } as PublicFormInstaceType); | |
25 | +</script> | |
26 | + | |
27 | +<template> | |
28 | + <BasicForm @register="register" class="max-w-3/4" /> | |
29 | +</template> | ... | ... |
1 | +export { default as DataSourceForm } from './index.vue'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { Spin, Tooltip } from 'ant-design-vue'; | |
3 | + import { | |
4 | + CopyOutlined, | |
5 | + SettingOutlined, | |
6 | + SwapOutlined, | |
7 | + DeleteOutlined, | |
8 | + } from '@ant-design/icons-vue'; | |
9 | + import { computed } from 'vue'; | |
10 | + import { PublicFormInstaceType, DataSourceType, SelectedWidgetKeys } from '../../index.type'; | |
11 | + import { fetchDatasourceComponent } from '../../../packages'; | |
12 | + import { ConfigType, CreateComponentType } from '../../../packages/index.type'; | |
13 | + import { ref } from 'vue'; | |
14 | + import { unref } from 'vue'; | |
15 | + import { watch } from 'vue'; | |
16 | + import { nextTick } from 'vue'; | |
17 | + import { useUpdateQueue } from './useUpdateQueue'; | |
18 | + import { useSort } from './useSort'; | |
19 | + import { SettingModal } from '../SettingModal'; | |
20 | + import { useModal } from '/@/components/Modal'; | |
21 | + import { ModalParamsType } from '/#/utils'; | |
22 | + import { DataActionModeEnum } from '/@/enums/toolEnum'; | |
23 | + import { toRaw } from 'vue'; | |
24 | + import cloneDeep from 'lodash-es/cloneDeep'; | |
25 | + import { isBoolean } from '/@/utils/is'; | |
26 | + import { DATA_SOURCE_LIMIT_NUMBER } from '../..'; | |
27 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
28 | + | |
29 | + const props = defineProps<{ | |
30 | + selectWidgetKeys: SelectedWidgetKeys; | |
31 | + dataSource: DataSourceType[]; | |
32 | + componentConfig: CreateComponentType; | |
33 | + }>(); | |
34 | + | |
35 | + const emit = defineEmits<{ | |
36 | + (event: 'update:dataSource', data: DataSourceType[]): void; | |
37 | + }>(); | |
38 | + | |
39 | + const { createMessage } = useMessage(); | |
40 | + | |
41 | + const [registerModal, { openModal }] = useModal(); | |
42 | + | |
43 | + const { trackUpdate, triggerUpdate } = useUpdateQueue(props); | |
44 | + | |
45 | + const spinning = ref(false); | |
46 | + | |
47 | + const dataSourceFormsEl = ref<{ uuid: string; instance: PublicFormInstaceType }[]>([]); | |
48 | + | |
49 | + const getComponent = computed(() => { | |
50 | + try { | |
51 | + const { componentKey } = props.selectWidgetKeys; | |
52 | + const component = fetchDatasourceComponent({ key: componentKey } as ConfigType); | |
53 | + return component; | |
54 | + } catch (error) { | |
55 | + return ''; | |
56 | + } | |
57 | + }); | |
58 | + | |
59 | + const hasSettingDesignIcon = computed(() => { | |
60 | + const { componetDesign } = props.componentConfig.persetOption || {}; | |
61 | + return isBoolean(componetDesign) ? componetDesign : true; | |
62 | + }); | |
63 | + | |
64 | + const setDataSourceFormsEl = (uuid: string, instance: PublicFormInstaceType, index) => { | |
65 | + const findIndex = unref(props.dataSource).findIndex((item) => item.uuid === uuid); | |
66 | + if (~findIndex) { | |
67 | + dataSourceFormsEl.value[index] = { uuid, instance }; | |
68 | + triggerUpdate(uuid, instance); | |
69 | + } | |
70 | + }; | |
71 | + | |
72 | + const getFormValueByUUID = (uuid: string): Recordable => { | |
73 | + const el = unref(dataSourceFormsEl).find((item) => item.uuid === uuid); | |
74 | + if (el && el.instance) return el.instance.getFormValues(); | |
75 | + return {}; | |
76 | + }; | |
77 | + | |
78 | + const getFormValues = (): DataSourceType[] => { | |
79 | + // 过滤失效form | |
80 | + dataSourceFormsEl.value = unref(dataSourceFormsEl).filter((item) => item.instance); | |
81 | + | |
82 | + return unref(dataSourceFormsEl).map((item) => { | |
83 | + const value = item.instance?.getFormValues(); | |
84 | + const oldValue = | |
85 | + props.dataSource.find((temp) => temp.uuid === item.uuid) || ({} as DataSourceType); | |
86 | + return { | |
87 | + componentInfo: toRaw(oldValue.componentInfo), | |
88 | + ...value, | |
89 | + uuid: item.uuid, | |
90 | + }; | |
91 | + }); | |
92 | + }; | |
93 | + | |
94 | + const setFormValues = (value: DataSourceType[]) => { | |
95 | + value.forEach((item) => { | |
96 | + const { uuid } = item; | |
97 | + const el = unref(dataSourceFormsEl).find((item) => item.uuid === uuid); | |
98 | + trackUpdate(uuid); | |
99 | + if (el && el.instance) { | |
100 | + triggerUpdate(uuid, el.instance); | |
101 | + } | |
102 | + }); | |
103 | + }; | |
104 | + | |
105 | + const validate = async () => { | |
106 | + try { | |
107 | + for (const item of unref(dataSourceFormsEl)) { | |
108 | + const errors = await item.instance?.validate?.(); | |
109 | + if (isBoolean(errors) && !errors) return { flag: false, errors }; | |
110 | + } | |
111 | + return { flag: true, errors: [] }; | |
112 | + } catch (error) { | |
113 | + console.error(error); | |
114 | + return { flag: false, errors: error }; | |
115 | + } | |
116 | + }; | |
117 | + | |
118 | + const resetFormValues = async () => { | |
119 | + dataSourceFormsEl.value = unref(dataSourceFormsEl).filter((item) => item.instance); | |
120 | + unref(dataSourceFormsEl).forEach((item) => { | |
121 | + item.instance && item.instance?.resetFormValues?.(); | |
122 | + }); | |
123 | + }; | |
124 | + | |
125 | + const handleCopy = (record: DataSourceType) => { | |
126 | + if (props.dataSource.length >= DATA_SOURCE_LIMIT_NUMBER) { | |
127 | + createMessage.warning('绑定的数据源不能超过10条~'); | |
128 | + return; | |
129 | + } | |
130 | + | |
131 | + const allValues = getFormValues(); | |
132 | + const currentRecord = getFormValueByUUID(record.uuid); | |
133 | + const uuid = trackUpdate(); | |
134 | + const raw = toRaw(record); | |
135 | + | |
136 | + emit('update:dataSource', [ | |
137 | + ...allValues, | |
138 | + { componentInfo: raw.componentInfo, ...currentRecord, uuid }, | |
139 | + ]); | |
140 | + }; | |
141 | + | |
142 | + const handleSetting = (record: DataSourceType) => { | |
143 | + openModal(true, { mode: DataActionModeEnum.UPDATE, record: record } as ModalParamsType); | |
144 | + }; | |
145 | + | |
146 | + const handleDelete = (record: DataSourceType) => { | |
147 | + const deleteElIndex = unref(dataSourceFormsEl).findIndex((item) => item.uuid === record.uuid); | |
148 | + unref(dataSourceFormsEl).splice(deleteElIndex, 1); | |
149 | + const raw = getFormValues(); | |
150 | + emit('update:dataSource', raw); | |
151 | + }; | |
152 | + | |
153 | + watch( | |
154 | + () => props.dataSource, | |
155 | + async (value) => { | |
156 | + if (value && value.length) { | |
157 | + nextTick(); | |
158 | + setFormValues(value); | |
159 | + } | |
160 | + } | |
161 | + ); | |
162 | + | |
163 | + const { containerEl } = useSort(emit, getFormValues); | |
164 | + | |
165 | + const handleSettingOk = (data: DataSourceType) => { | |
166 | + const { uuid } = data; | |
167 | + const _dataSource = cloneDeep(props.dataSource); | |
168 | + | |
169 | + const index = _dataSource.findIndex((item) => item.uuid === uuid); | |
170 | + | |
171 | + _dataSource[index] = { ..._dataSource[index], ...data }; | |
172 | + | |
173 | + emit('update:dataSource', _dataSource); | |
174 | + }; | |
175 | + | |
176 | + defineExpose({ | |
177 | + getFormValues, | |
178 | + validate, | |
179 | + setFormValues, | |
180 | + resetFormValues, | |
181 | + } as PublicFormInstaceType); | |
182 | +</script> | |
183 | + | |
184 | +<template> | |
185 | + <section ref="containerEl"> | |
186 | + <Spin :spinning="spinning"> | |
187 | + <main v-for="(item, index) in dataSource" :key="item.uuid" class="flex"> | |
188 | + <label class="w-24 text-right pr-2">数据源{{ index + 1 }}</label> | |
189 | + <component | |
190 | + :ref="(event) => setDataSourceFormsEl(item.uuid, event, index)" | |
191 | + class="flex-1 bg-light-50 dark:bg-dark-400" | |
192 | + :is="getComponent" | |
193 | + :component-config="componentConfig" | |
194 | + :values="item" | |
195 | + /> | |
196 | + <div class="w-28 flex gap-3 ml-2"> | |
197 | + <Tooltip title="复制"> | |
198 | + <CopyOutlined @click="handleCopy(item)" class="cursor-pointer text-lg !leading-32px" /> | |
199 | + </Tooltip> | |
200 | + <Tooltip title="设置" v-if="hasSettingDesignIcon"> | |
201 | + <SettingOutlined | |
202 | + @click="handleSetting(item)" | |
203 | + class="cursor-pointer text-lg !leading-32px" | |
204 | + /> | |
205 | + </Tooltip> | |
206 | + <Tooltip title="拖拽排序"> | |
207 | + <SwapOutlined | |
208 | + class="cursor-pointer text-lg !leading-32px svg:transform svg:rotate-90 sort-icon" | |
209 | + /> | |
210 | + </Tooltip> | |
211 | + <Tooltip title="删除"> | |
212 | + <DeleteOutlined | |
213 | + @click="handleDelete(item)" | |
214 | + class="cursor-pointer text-lg !leading-32px" | |
215 | + /> | |
216 | + </Tooltip> | |
217 | + </div> | |
218 | + </main> | |
219 | + </Spin> | |
220 | + | |
221 | + <SettingModal | |
222 | + @register="registerModal" | |
223 | + @ok="handleSettingOk" | |
224 | + :component-config="componentConfig" | |
225 | + :select-widget-keys="selectWidgetKeys" | |
226 | + /> | |
227 | + </section> | |
228 | +</template> | ... | ... |
1 | +import { nextTick, onMounted, ref, unref } from 'vue'; | |
2 | +import { useSortable } from '/@/hooks/web/useSortable'; | |
3 | +import { isNullAndUnDef } from '/@/utils/is'; | |
4 | +import { DataSourceType } from '../../index.type'; | |
5 | + | |
6 | +export const useSort = ( | |
7 | + emit: (event: 'update:dataSource', data: DataSourceType[]) => void, | |
8 | + getFormValues: () => DataSourceType[] | |
9 | +) => { | |
10 | + let inited = false; | |
11 | + const containerEl = ref<Nullable<HTMLElement>>(null); | |
12 | + async function handleSort() { | |
13 | + if (inited) return; | |
14 | + await nextTick(); | |
15 | + const container = unref(containerEl); | |
16 | + if (!container) return; | |
17 | + const element: Nullable<HTMLElement> = unref(container).querySelector('.ant-spin-container'); | |
18 | + if (!element) return; | |
19 | + | |
20 | + const { initSortable } = useSortable(element, { | |
21 | + handle: '.sort-icon', | |
22 | + onEnd: (evt) => { | |
23 | + const { oldIndex, newIndex } = evt; | |
24 | + if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) { | |
25 | + return; | |
26 | + } | |
27 | + const dataSource = getFormValues(); | |
28 | + | |
29 | + if (oldIndex > newIndex) { | |
30 | + dataSource.splice(newIndex, 0, dataSource[oldIndex]); | |
31 | + dataSource.splice(oldIndex + 1, 1); | |
32 | + } else { | |
33 | + dataSource.splice(newIndex + 1, 0, dataSource[oldIndex]); | |
34 | + dataSource.splice(oldIndex, 1); | |
35 | + } | |
36 | + emit('update:dataSource', dataSource); | |
37 | + }, | |
38 | + }); | |
39 | + initSortable(); | |
40 | + inited = true; | |
41 | + } | |
42 | + | |
43 | + onMounted(() => handleSort()); | |
44 | + | |
45 | + return { containerEl }; | |
46 | +}; | ... | ... |
1 | +import { nextTick } from 'vue'; | |
2 | +import { PublicFormInstaceType, DataSourceType } from '../../index.type'; | |
3 | +import { buildUUID } from '/@/utils/uuid'; | |
4 | + | |
5 | +export const useUpdateQueue = (props: { dataSource: DataSourceType[] }) => { | |
6 | + const needUpdateQueue: string[] = []; | |
7 | + | |
8 | + const triggerUpdate = (uuid: string, instance: PublicFormInstaceType) => { | |
9 | + const index = needUpdateQueue.findIndex((item) => item === uuid); | |
10 | + if (~index) { | |
11 | + const value = props.dataSource.find((item) => item.uuid === uuid); | |
12 | + nextTick(() => instance?.setFormValues(value || {})); | |
13 | + needUpdateQueue.splice(index, 1); | |
14 | + } | |
15 | + }; | |
16 | + | |
17 | + const trackUpdate = (uuid = buildUUID()) => { | |
18 | + needUpdateQueue.push(uuid); | |
19 | + return uuid; | |
20 | + }; | |
21 | + | |
22 | + return { trackUpdate, triggerUpdate }; | |
23 | +}; | ... | ... |
1 | +export { default as MessageAlert } from './index.vue'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { computed } from 'vue'; | |
3 | + import { PackagesCategoryEnum } from '../../../packages/index.type'; | |
4 | + import { SelectedWidgetKeys } from '../../index.type'; | |
5 | + import { Alert } from 'ant-design-vue'; | |
6 | + | |
7 | + const props = defineProps<{ | |
8 | + selectWidgetKeys: SelectedWidgetKeys; | |
9 | + }>(); | |
10 | + | |
11 | + const alert = { | |
12 | + [PackagesCategoryEnum.MAP]: [ | |
13 | + '地图组件,需绑定两个数据源,且数据源为同一设备。第一数据源为经度,第二数据源为纬度,否则地图组件不能正常显示。', | |
14 | + ], | |
15 | + [PackagesCategoryEnum.CONTROL]: [ | |
16 | + '控制组件数据源为TCP产品,则其控制命令下发为TCP产品 物模型=>服务,且不具备状态显示功能.', | |
17 | + '控制组件数据源为非TCP产品,则其控制命令下发为产品 物模型=>属性,且具备状态显示功能.', | |
18 | + ], | |
19 | + }; | |
20 | + | |
21 | + const getMessage = computed(() => { | |
22 | + const { selectWidgetKeys } = props; | |
23 | + const { categoryKey } = selectWidgetKeys; | |
24 | + return alert[categoryKey]; | |
25 | + }); | |
26 | +</script> | |
27 | + | |
28 | +<template> | |
29 | + <Alert v-if="getMessage" type="info" show-icon> | |
30 | + <template #description> | |
31 | + <div v-for="(item, index) in getMessage" :key="index">{{ item }}</div> | |
32 | + </template> | |
33 | + </Alert> | |
34 | +</template> | ... | ... |
1 | +export { default as SettingModal } from './index.vue'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { computed } from 'vue'; | |
3 | + import { fetchConfigComponent } from '../../../packages'; | |
4 | + import { DataSourceType, PublicFormInstaceType, SelectedWidgetKeys } from '../../index.type'; | |
5 | + import { BasicModal, useModalInner } from '/@/components/Modal'; | |
6 | + import { ConfigType, CreateComponentType } from '../../../packages/index.type'; | |
7 | + import { ref } from 'vue'; | |
8 | + import { unref } from 'vue'; | |
9 | + import { ModalParamsType } from '/#/utils'; | |
10 | + | |
11 | + const props = defineProps<{ | |
12 | + selectWidgetKeys: SelectedWidgetKeys; | |
13 | + componentConfig: CreateComponentType; | |
14 | + }>(); | |
15 | + | |
16 | + const emit = defineEmits(['register', 'ok']); | |
17 | + | |
18 | + const settingFormEl = ref<Nullable<PublicFormInstaceType>>(null); | |
19 | + | |
20 | + const getSettingComponent = computed(() => { | |
21 | + try { | |
22 | + const { componentKey } = props.selectWidgetKeys; | |
23 | + const component = fetchConfigComponent({ key: componentKey } as ConfigType); | |
24 | + return component; | |
25 | + } catch (error) { | |
26 | + return ''; | |
27 | + } | |
28 | + }); | |
29 | + | |
30 | + const currentEditRecord = ref<DataSourceType>({} as DataSourceType); | |
31 | + | |
32 | + const [register, { closeModal }] = useModalInner((data: ModalParamsType<DataSourceType>) => { | |
33 | + const { record } = data; | |
34 | + currentEditRecord.value = record; | |
35 | + setFormValues(record.componentInfo || {}); | |
36 | + }); | |
37 | + | |
38 | + const getFormValues = () => { | |
39 | + return unref(settingFormEl)?.getFormValues(); | |
40 | + }; | |
41 | + | |
42 | + const setFormValues = (data: Recordable) => { | |
43 | + unref(settingFormEl)?.setFormValues(data || {}); | |
44 | + }; | |
45 | + | |
46 | + const handleOk = () => { | |
47 | + const { uuid } = unref(currentEditRecord); | |
48 | + emit('ok', { uuid, componentInfo: getFormValues() } as DataSourceType); | |
49 | + closeModal(); | |
50 | + }; | |
51 | + | |
52 | + defineExpose({ | |
53 | + getFormValues, | |
54 | + setFormValues, | |
55 | + } as PublicFormInstaceType); | |
56 | +</script> | |
57 | + | |
58 | +<template> | |
59 | + <BasicModal @register="register" title="组件设置" @ok="handleOk"> | |
60 | + <!-- --> | |
61 | + <component ref="settingFormEl" :is="getSettingComponent" /> | |
62 | + </BasicModal> | |
63 | +</template> | ... | ... |
1 | +import { ValidateErrorEntity } from 'ant-design-vue/es/form/interface'; | |
2 | +import { DataSource } from '../palette/types'; | |
3 | + | |
4 | +export interface SelectedWidgetKeys { | |
5 | + categoryKey: string; | |
6 | + componentKey: string; | |
7 | +} | |
8 | + | |
9 | +export interface DataSourceType { | |
10 | + uuid: string; | |
11 | + componentInfo?: Recordable; | |
12 | + [key: string]: any; | |
13 | +} | |
14 | + | |
15 | +export interface PublicComponentValueType extends DataSource { | |
16 | + uuid: string; | |
17 | + [key: string]: any; | |
18 | +} | |
19 | + | |
20 | +export interface PublicFormInstaceType { | |
21 | + getFormValues: () => Recordable; | |
22 | + setFormValues: (data: Recordable) => void; | |
23 | + resetFormValues: () => Promise<void>; | |
24 | + validate?: () => Promise<{ flag: boolean; errors: ValidateErrorEntity }>; | |
25 | +} | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { computed, ref, watch } from 'vue'; | |
3 | + import { BasicInfoForm } from './components/BasicInfoForm'; | |
4 | + import { ModalParamsType } from '/#/utils'; | |
5 | + import { BasicModal, useModalInner } from '/@/components/Modal'; | |
6 | + import { Divider, Tabs, Button, Spin } from 'ant-design-vue'; | |
7 | + import { DataSourceForm } from './components/DataSourceForm'; | |
8 | + import { WidgetLibrary } from '../widgetLibrary'; | |
9 | + import { | |
10 | + CreateComponentType, | |
11 | + PackagesCategoryEnum, | |
12 | + PackagesCategoryNameEnum, | |
13 | + } from '../packages/index.type'; | |
14 | + import { TextComponent1Config } from '../packages/components/Text/TextComponent1'; | |
15 | + import { DataSourceType, SelectedWidgetKeys } from './index.type'; | |
16 | + import { buildUUID } from '/@/utils/uuid'; | |
17 | + import { unref } from 'vue'; | |
18 | + import { AddDataComponentParams } from '/@/api/dataBoard/model'; | |
19 | + import { useCalcNewWidgetPosition } from '../palette/hooks/useCalcNewWidgetPosition'; | |
20 | + import { Layout } from 'vue3-grid-layout'; | |
21 | + import { useBoardId } from '../palette/hooks/useBoardId'; | |
22 | + import { addDataComponent, updateDataComponent } from '/@/api/dataBoard'; | |
23 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
24 | + import { DataActionModeEnum } from '/@/enums/toolEnum'; | |
25 | + import { WidgetDataType } from '../palette/hooks/useDataSource'; | |
26 | + import { DATA_SOURCE_LIMIT_NUMBER } from '.'; | |
27 | + import { DataSource } from '../palette/types'; | |
28 | + import { useGetComponentConfig } from '../packages/hook/useGetComponetConfig'; | |
29 | + import { MessageAlert } from './components/MessageAlert'; | |
30 | + import { createSelectWidgetKeysContext, createSelectWidgetModeContext } from './useContext'; | |
31 | + | |
32 | + const props = defineProps<{ | |
33 | + layout: Layout[]; | |
34 | + }>(); | |
35 | + | |
36 | + const emit = defineEmits(['register', 'ok']); | |
37 | + | |
38 | + enum TabKeyEnum { | |
39 | + BASIC = 'basic', | |
40 | + VISUAL = 'visual', | |
41 | + } | |
42 | + | |
43 | + const { boardId } = useBoardId(); | |
44 | + | |
45 | + const { createMessage } = useMessage(); | |
46 | + | |
47 | + const loading = ref(false); | |
48 | + | |
49 | + const dataSourceFormSpinning = ref(false); | |
50 | + | |
51 | + const selectWidgetKeys = ref<SelectedWidgetKeys>({ | |
52 | + componentKey: TextComponent1Config.key, | |
53 | + categoryKey: PackagesCategoryEnum.TEXT, | |
54 | + }); | |
55 | + | |
56 | + createSelectWidgetKeysContext(selectWidgetKeys); | |
57 | + | |
58 | + const getComponentConfig = computed<CreateComponentType>(() => { | |
59 | + return useGetComponentConfig(unref(selectWidgetKeys).componentKey); | |
60 | + }); | |
61 | + | |
62 | + const activeKey = ref(TabKeyEnum.BASIC); | |
63 | + | |
64 | + const genNewDataSourceItem = () => { | |
65 | + return { | |
66 | + uuid: buildUUID(), | |
67 | + componentInfo: unref(getComponentConfig).persetOption || {}, | |
68 | + } as DataSourceType; | |
69 | + }; | |
70 | + | |
71 | + const dataSource = ref<DataSourceType[]>(Array.from({ length: 1 }, () => genNewDataSourceItem())); | |
72 | + | |
73 | + const currentMode = ref<DataActionModeEnum>(DataActionModeEnum.CREATE); | |
74 | + | |
75 | + createSelectWidgetModeContext(currentMode); | |
76 | + | |
77 | + const currentRecord = ref<WidgetDataType>(); | |
78 | + | |
79 | + const [registerModal, { closeModal }] = useModalInner( | |
80 | + (params: ModalParamsType<WidgetDataType>) => { | |
81 | + resetFormValues(); | |
82 | + const { mode, record } = params; | |
83 | + currentMode.value = mode; | |
84 | + currentRecord.value = record; | |
85 | + if (mode === DataActionModeEnum.UPDATE) { | |
86 | + const config = useGetComponentConfig(record.frontId); | |
87 | + selectWidgetKeys.value = { | |
88 | + componentKey: config.componentConfig.key, | |
89 | + categoryKey: config.componentConfig.package, | |
90 | + }; | |
91 | + setFormValues(record); | |
92 | + } else { | |
93 | + dataSource.value = [genNewDataSourceItem()]; | |
94 | + } | |
95 | + } | |
96 | + ); | |
97 | + | |
98 | + const basicInfoFromEl = ref<Nullable<InstanceType<typeof BasicInfoForm>>>(null); | |
99 | + | |
100 | + const dataSourceFormEl = ref<Nullable<InstanceType<typeof DataSourceForm>>>(null); | |
101 | + | |
102 | + const handleTabsChange = (activeKey: TabKeyEnum) => { | |
103 | + if (activeKey === TabKeyEnum.VISUAL) { | |
104 | + dataSource.value = (dataSourceFormEl.value?.getFormValues() as DataSourceType[]) || []; | |
105 | + } | |
106 | + }; | |
107 | + | |
108 | + const handleNewRecord = () => { | |
109 | + if (unref(dataSource).length >= DATA_SOURCE_LIMIT_NUMBER) { | |
110 | + createMessage.warning('绑定的数据源不能超过10条~'); | |
111 | + return; | |
112 | + } | |
113 | + dataSource.value.push(genNewDataSourceItem()); | |
114 | + }; | |
115 | + | |
116 | + /** | |
117 | + * @description 可视化组件变化 数据源组件变更 重新赋值表单 | |
118 | + */ | |
119 | + watch( | |
120 | + () => selectWidgetKeys.value.componentKey, | |
121 | + (value) => { | |
122 | + if (value) { | |
123 | + dataSource.value = unref(dataSource).map((item) => ({ | |
124 | + ...item, | |
125 | + componentInfo: { ...unref(getComponentConfig).persetOption, ...item.componentInfo }, | |
126 | + })); | |
127 | + if (window.requestIdleCallback as unknown as boolean) { | |
128 | + requestIdleCallback( | |
129 | + () => { | |
130 | + setFormValues({ dataSource: unref(dataSource) } as WidgetDataType); | |
131 | + }, | |
132 | + { timeout: 500 } | |
133 | + ); | |
134 | + } else { | |
135 | + setTimeout(() => { | |
136 | + setFormValues({ dataSource: unref(dataSource) } as WidgetDataType); | |
137 | + }, 500); | |
138 | + } | |
139 | + } | |
140 | + } | |
141 | + ); | |
142 | + | |
143 | + const validate = async () => { | |
144 | + return await unref(dataSourceFormEl)?.validate?.(); | |
145 | + }; | |
146 | + | |
147 | + const resetFormValues = () => { | |
148 | + unref(basicInfoFromEl)?.resetFormValues(); | |
149 | + unref(dataSourceFormEl)?.resetFormValues(); | |
150 | + }; | |
151 | + | |
152 | + const setFormValues = (data: WidgetDataType) => { | |
153 | + const { dataSource: newDataSource } = data; | |
154 | + const { name, remark } = unref(currentRecord) || {}; | |
155 | + dataSource.value = newDataSource; | |
156 | + unref(basicInfoFromEl)?.setFormValues({ name, remark }); | |
157 | + dataSourceFormSpinning.value = true; | |
158 | + setTimeout(() => { | |
159 | + unref(dataSourceFormEl)?.setFormValues(newDataSource); | |
160 | + dataSourceFormSpinning.value = false; | |
161 | + }, 500); | |
162 | + }; | |
163 | + | |
164 | + const getFormValues = () => { | |
165 | + const dataSource = ( | |
166 | + (unref(dataSourceFormEl)?.getFormValues() as unknown as DataSource[]) || [] | |
167 | + ).map((item) => { | |
168 | + Reflect.deleteProperty(item, 'uuid'); | |
169 | + return item; | |
170 | + }); | |
171 | + | |
172 | + const basicInfo = unref(basicInfoFromEl)?.getFormValues(); | |
173 | + | |
174 | + const layout = useCalcNewWidgetPosition(props.layout); | |
175 | + | |
176 | + const frontId = unref(selectWidgetKeys).componentKey; | |
177 | + return { | |
178 | + boardId: unref(boardId), | |
179 | + record: { | |
180 | + ...(unref(currentMode) === DataActionModeEnum.UPDATE | |
181 | + ? { id: unref(currentRecord)?.id } | |
182 | + : {}), | |
183 | + ...basicInfo, | |
184 | + dataSource, | |
185 | + layout, | |
186 | + frontId, | |
187 | + }, | |
188 | + } as AddDataComponentParams; | |
189 | + }; | |
190 | + | |
191 | + const getVisualConfigTitle = computed(() => { | |
192 | + const { categoryKey } = unref(selectWidgetKeys); | |
193 | + const category = PackagesCategoryNameEnum[PackagesCategoryEnum[categoryKey]]; | |
194 | + const { componentConfig } = unref(getComponentConfig); | |
195 | + return `${category} / ${componentConfig.title}`; | |
196 | + }); | |
197 | + | |
198 | + const handleSubmit = async () => { | |
199 | + const validateResult = await validate(); | |
200 | + if (validateResult && !validateResult.flag) { | |
201 | + const { errors } = validateResult; | |
202 | + if (errors && errors.errorFields.length) { | |
203 | + const errorRecord = errors.errorFields[0]; | |
204 | + createMessage.warning(errorRecord.errors.join('')); | |
205 | + if (activeKey.value === TabKeyEnum.VISUAL) { | |
206 | + activeKey.value = TabKeyEnum.BASIC; | |
207 | + } | |
208 | + return; | |
209 | + } | |
210 | + } | |
211 | + const value = getFormValues(); | |
212 | + try { | |
213 | + loading.value = true; | |
214 | + unref(currentMode) === DataActionModeEnum.UPDATE | |
215 | + ? await updateDataComponent(value) | |
216 | + : await addDataComponent(value); | |
217 | + createMessage.success( | |
218 | + `${unref(currentMode) === DataActionModeEnum.UPDATE ? '编辑' : '新增'}成功~` | |
219 | + ); | |
220 | + closeModal(); | |
221 | + emit('ok'); | |
222 | + } catch (error) { | |
223 | + throw error; | |
224 | + } finally { | |
225 | + loading.value = false; | |
226 | + } | |
227 | + }; | |
228 | +</script> | |
229 | + | |
230 | +<template> | |
231 | + <BasicModal | |
232 | + title="自定义组件" | |
233 | + width="70%" | |
234 | + @register="registerModal" | |
235 | + @ok="handleSubmit" | |
236 | + :ok-button-props="{ loading }" | |
237 | + > | |
238 | + <Tabs v-model:active-key="activeKey" type="card" @change="handleTabsChange" :animated="true"> | |
239 | + <Tabs.TabPane tab="基础配置" :key="TabKeyEnum.BASIC"> | |
240 | + <Divider orientation="left">基础信息</Divider> | |
241 | + | |
242 | + <BasicInfoForm ref="basicInfoFromEl" /> | |
243 | + | |
244 | + <MessageAlert :select-widget-keys="selectWidgetKeys" /> | |
245 | + | |
246 | + <Divider orientation="left">数据源配置</Divider> | |
247 | + | |
248 | + <Spin :spinning="dataSourceFormSpinning"> | |
249 | + <DataSourceForm | |
250 | + ref="dataSourceFormEl" | |
251 | + :key="getComponentConfig.componentConfig.datasourceConKey" | |
252 | + :select-widget-keys="selectWidgetKeys" | |
253 | + v-model:dataSource="dataSource" | |
254 | + :component-config="getComponentConfig" | |
255 | + /> | |
256 | + </Spin> | |
257 | + | |
258 | + <div class="flex justify-center"> | |
259 | + <Button type="primary" @click="handleNewRecord">添加数据源</Button> | |
260 | + </div> | |
261 | + </Tabs.TabPane> | |
262 | + <Tabs.TabPane :key="TabKeyEnum.VISUAL"> | |
263 | + <template #tab> | |
264 | + <span>可视化配置</span> | |
265 | + <span class="mx-1">-</span> | |
266 | + <span> {{ getVisualConfigTitle }}</span> | |
267 | + </template> | |
268 | + <WidgetLibrary v-model:checked="selectWidgetKeys" /> | |
269 | + </Tabs.TabPane> | |
270 | + </Tabs> | |
271 | + </BasicModal> | |
272 | +</template> | ... | ... |
1 | +import { Ref, inject, provide } from 'vue'; | |
2 | +import { SelectedWidgetKeys } from './index.type'; | |
3 | +import { DataActionModeEnum } from '/@/enums/toolEnum'; | |
4 | + | |
5 | +const selectWidgetKeysKey = Symbol('select-widget-keys'); | |
6 | + | |
7 | +export const createSelectWidgetKeysContext = (state: Ref<SelectedWidgetKeys>) => { | |
8 | + provide(selectWidgetKeysKey, state); | |
9 | +}; | |
10 | + | |
11 | +export const useSelectWidgetKeys = () => { | |
12 | + return inject(selectWidgetKeysKey) as Ref<SelectedWidgetKeys>; | |
13 | +}; | |
14 | + | |
15 | +const selectWidgetModeKey = Symbol('select-widget-mode'); | |
16 | + | |
17 | +export const createSelectWidgetModeContext = (mode: Ref<DataActionModeEnum>) => { | |
18 | + provide(selectWidgetModeKey, mode); | |
19 | +}; | |
20 | + | |
21 | +export const useSelectWidgetMode = () => { | |
22 | + return inject(selectWidgetModeKey) as Ref<DataActionModeEnum>; | |
23 | +}; | ... | ... |
src/views/visual/packages/componentMap.ts
0 → 100644
1 | +import { tryOnUnmounted } from '@vueuse/core'; | |
2 | +import { Component } from 'vue'; | |
3 | + | |
4 | +/** | |
5 | + * @description 转换前端组件key, 兼容旧数据 | |
6 | + * @param string | |
7 | + */ | |
8 | +export const transformComponentKey = (string: string) => { | |
9 | + const CONNECTION_SYMBOL = '-'; | |
10 | + const needTransformFlag = string.includes(CONNECTION_SYMBOL); | |
11 | + | |
12 | + if (needTransformFlag) { | |
13 | + return string | |
14 | + .split(CONNECTION_SYMBOL) | |
15 | + .map((item) => `${item.substring(0, 1).toUpperCase()}${item.substring(1).toLowerCase()}`) | |
16 | + .join(''); | |
17 | + } | |
18 | + | |
19 | + return string; | |
20 | +}; | |
21 | + | |
22 | +export const componentMap = new Map(); | |
23 | + | |
24 | +export const registerComponent = (name: string, component: Component) => { | |
25 | + const _name = transformComponentKey(name); | |
26 | + if (componentMap.has(_name)) { | |
27 | + return componentMap.get(_name); | |
28 | + } | |
29 | + | |
30 | + componentMap.set(_name, component); | |
31 | + | |
32 | + tryOnUnmounted(() => { | |
33 | + uninstallComponent(_name); | |
34 | + }); | |
35 | + | |
36 | + return component; | |
37 | +}; | |
38 | + | |
39 | +export const uninstallComponent = (name: string) => { | |
40 | + componentMap.delete(transformComponentKey(name)); | |
41 | +}; | |
42 | + | |
43 | +export const getComponent = (frontId: string) => { | |
44 | + return componentMap.get(transformComponentKey(frontId)); | |
45 | +}; | ... | ... |
1 | +import cloneDeep from 'lodash-es/cloneDeep'; | |
2 | +import { ControlComponentSlidingSwitchConfig } from '.'; | |
3 | +import { | |
4 | + ConfigType, | |
5 | + CreateComponentType, | |
6 | + PublicComponentOptions, | |
7 | + PublicPresetOptions, | |
8 | +} from '/@/views/visual/packages/index.type'; | |
9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | |
10 | + | |
11 | +export const option: PublicPresetOptions = { | |
12 | + componetDesign: false, | |
13 | +}; | |
14 | + | |
15 | +export default class Config extends PublicConfigClass implements CreateComponentType { | |
16 | + public key: string = ControlComponentSlidingSwitchConfig.key; | |
17 | + | |
18 | + public attr = { ...componentInitConfig }; | |
19 | + | |
20 | + public componentConfig: ConfigType = cloneDeep(ControlComponentSlidingSwitchConfig); | |
21 | + | |
22 | + public persetOption = cloneDeep(option); | |
23 | + | |
24 | + public option: PublicComponentOptions; | |
25 | + | |
26 | + constructor(option: PublicComponentOptions) { | |
27 | + super(); | |
28 | + this.option = { ...option }; | |
29 | + } | |
30 | +} | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | |
3 | + import { useForm, BasicForm } from '/@/components/Form'; | |
4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
5 | + | |
6 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | |
7 | + schemas: [ | |
8 | + { | |
9 | + field: ComponentConfigFieldEnum.FONT_COLOR, | |
10 | + label: '数值字体颜色', | |
11 | + component: 'ColorPicker', | |
12 | + changeEvent: 'update:value', | |
13 | + componentProps: { | |
14 | + defaultValue: '#FD7347', | |
15 | + }, | |
16 | + }, | |
17 | + { | |
18 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
19 | + label: '显示设备名称', | |
20 | + component: 'Checkbox', | |
21 | + }, | |
22 | + ], | |
23 | + showActionButtonGroup: false, | |
24 | + labelWidth: 120, | |
25 | + baseColProps: { | |
26 | + span: 12, | |
27 | + }, | |
28 | + }); | |
29 | + | |
30 | + const getFormValues = () => { | |
31 | + return getFieldsValue(); | |
32 | + }; | |
33 | + | |
34 | + const setFormValues = (data: Recordable) => { | |
35 | + return setFieldsValue(data); | |
36 | + }; | |
37 | + | |
38 | + defineExpose({ | |
39 | + getFormValues, | |
40 | + setFormValues, | |
41 | + resetFormValues: resetFields, | |
42 | + } as PublicFormInstaceType); | |
43 | +</script> | |
44 | + | |
45 | +<template> | |
46 | + <BasicForm @register="register" /> | |
47 | +</template> | ... | ... |
src/views/visual/packages/components/Control/ControlComponentSlidingSwitch/datasource.vue
0 → 100644
1 | +<script lang="ts" setup> | |
2 | + import { CreateComponentType } from '/@/views/visual/packages/index.type'; | |
3 | + import { BasicForm, useForm } from '/@/components/Form'; | |
4 | + import { | |
5 | + PublicComponentValueType, | |
6 | + PublicFormInstaceType, | |
7 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
8 | + import { | |
9 | + CommonDataSourceBindValueType, | |
10 | + commonDataSourceSchemas, | |
11 | + } from '../../../config/common.config'; | |
12 | + import { DataSource } from '/@/views/visual/palette/types'; | |
13 | + | |
14 | + defineProps<{ | |
15 | + values: PublicComponentValueType; | |
16 | + componentConfig: CreateComponentType; | |
17 | + }>(); | |
18 | + | |
19 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | |
20 | + labelWidth: 0, | |
21 | + showActionButtonGroup: false, | |
22 | + layout: 'horizontal', | |
23 | + labelCol: { span: 0 }, | |
24 | + schemas: commonDataSourceSchemas(), | |
25 | + }); | |
26 | + | |
27 | + const getFormValues = () => { | |
28 | + let value = getFieldsValue() as CommonDataSourceBindValueType; | |
29 | + value = { | |
30 | + ...value, | |
31 | + customCommand: { | |
32 | + transportType: value.transportType, | |
33 | + service: value.service, | |
34 | + command: value.command, | |
35 | + commandType: value.commandType, | |
36 | + }, | |
37 | + }; | |
38 | + return value; | |
39 | + }; | |
40 | + | |
41 | + const setFormValues = (record: DataSource) => { | |
42 | + const { customCommand } = record; | |
43 | + return setFieldsValue({ | |
44 | + ...record, | |
45 | + transportType: customCommand?.transportType, | |
46 | + service: customCommand?.service, | |
47 | + command: customCommand?.command, | |
48 | + commandType: customCommand?.commandType, | |
49 | + } as unknown as Partial<CommonDataSourceBindValueType>); | |
50 | + }; | |
51 | + | |
52 | + defineExpose({ | |
53 | + getFormValues, | |
54 | + setFormValues, | |
55 | + validate, | |
56 | + resetFormValues: resetFields, | |
57 | + } as PublicFormInstaceType); | |
58 | +</script> | |
59 | + | |
60 | +<template> | |
61 | + <BasicForm @register="register" /> | |
62 | +</template> | ... | ... |
1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | |
2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | |
3 | + | |
4 | +const componentKeys = useComponentKeys('ControlComponentSlidingSwitch'); | |
5 | + | |
6 | +export const ControlComponentSlidingSwitchConfig: ConfigType = { | |
7 | + ...componentKeys, | |
8 | + title: '控制组件2', | |
9 | + package: PackagesCategoryEnum.CONTROL, | |
10 | +}; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type'; | |
3 | + import { option } from './config'; | |
4 | + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket'; | |
5 | + import { Spin } from 'ant-design-vue'; | |
6 | + import { ref } from 'vue'; | |
7 | + import { useComponentScale } from '../../../hook/useComponentScale'; | |
8 | + import { useSendCommand } from '../../../hook/useSendCommand'; | |
9 | + const props = defineProps<{ | |
10 | + config: ComponentPropsConfigType<typeof option>; | |
11 | + }>(); | |
12 | + | |
13 | + const { getScale } = useComponentScale(props); | |
14 | + | |
15 | + const currentValue = ref(false); | |
16 | + | |
17 | + const { sendCommand, loading } = useSendCommand(); | |
18 | + const handleChange = async (event: Event) => { | |
19 | + const target = event.target as HTMLInputElement; | |
20 | + const value = target.checked; | |
21 | + | |
22 | + const flag = await sendCommand(props.config.option, value); | |
23 | + if (flag) currentValue.value = value; | |
24 | + flag ? (currentValue.value = value) : (target.checked = !value); | |
25 | + }; | |
26 | + | |
27 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | |
28 | + const { data = {} } = message; | |
29 | + const [latest] = data[attribute] || []; | |
30 | + const [_, value] = latest; | |
31 | + currentValue.value = !!value; | |
32 | + }; | |
33 | + | |
34 | + useDataFetch(props, updateFn); | |
35 | +</script> | |
36 | + | |
37 | +<template> | |
38 | + <main class="w-full h-full flex flex-col justify-center items-center"> | |
39 | + <Spin :spinning="loading"> | |
40 | + <label class="sliding-switch" :style="getScale"> | |
41 | + <input | |
42 | + type="checkbox" | |
43 | + :value="currentValue" | |
44 | + :checked="currentValue" | |
45 | + @change="handleChange" | |
46 | + /> | |
47 | + <span class="slider"></span> | |
48 | + <span class="on">ON</span> | |
49 | + <span class="off">OFF</span> | |
50 | + </label> | |
51 | + <div class="text-center mt-2 text-gray-700" :style="getScale"> 属性 </div> | |
52 | + </Spin> | |
53 | + </main> | |
54 | +</template> | |
55 | + | |
56 | +<style scoped lang="less"> | |
57 | + :deep(.ant-spin-container) { | |
58 | + @apply !flex !flex-col justify-center items-center !flex-nowrap; | |
59 | + } | |
60 | + | |
61 | + .sliding-switch { | |
62 | + position: relative; | |
63 | + display: block; | |
64 | + font-weight: 700; | |
65 | + line-height: 40px; | |
66 | + width: 80px; | |
67 | + height: 40px; | |
68 | + font-size: 14px; | |
69 | + cursor: pointer; | |
70 | + user-select: none; | |
71 | + | |
72 | + input[type='checkbox'] { | |
73 | + display: none; | |
74 | + } | |
75 | + | |
76 | + .slider { | |
77 | + width: 80px; | |
78 | + height: 40px; | |
79 | + display: flex; | |
80 | + align-items: center; | |
81 | + box-sizing: border-box; | |
82 | + border: 2px solid #ecf0f3; | |
83 | + border-radius: 20px; | |
84 | + box-shadow: -2px -2px 8px #fff, -2px -2px 12px hsl(0deg 0% 100% / 50%), | |
85 | + inset -2px -2px 8px #fff, inset -2px -2px 12px hsl(0deg 0% 100% / 50%), | |
86 | + inset 2px 2px 4px hsl(0deg 0% 100% / 10%), inset 2px 2px 8px rgb(0 0 0 / 30%), | |
87 | + 2px 2px 8px rgb(0 0 0 / 30%); | |
88 | + background-color: #ecf0f3; | |
89 | + z-index: -1; | |
90 | + } | |
91 | + | |
92 | + .slider::after { | |
93 | + cursor: pointer; | |
94 | + display: block; | |
95 | + content: ''; | |
96 | + width: 24px; | |
97 | + height: 24px; | |
98 | + border-radius: 50%; | |
99 | + margin-left: 6px; | |
100 | + margin-right: 6px; | |
101 | + background-color: #ecf0f3; | |
102 | + box-shadow: -2px -2px 8px #fff, -2px -2px 12px hsl(0deg 0% 100% / 50%), | |
103 | + inset 2px 2px 4px hsl(0deg 0% 100% / 10%), 2px 2px 8px rgb(0 0 0 / 30%); | |
104 | + z-index: 999; | |
105 | + transition: 0.5s; | |
106 | + } | |
107 | + | |
108 | + input:checked ~ .off { | |
109 | + opacity: 0; | |
110 | + } | |
111 | + | |
112 | + input:checked ~ .slider::after { | |
113 | + transform: translateX(35px); | |
114 | + } | |
115 | + | |
116 | + input:not(:checked) ~ .on { | |
117 | + opacity: 0; | |
118 | + transform: translateX(0); | |
119 | + } | |
120 | + | |
121 | + .on, | |
122 | + .off { | |
123 | + position: absolute; | |
124 | + top: 0; | |
125 | + display: inline-block; | |
126 | + margin-left: 3px; | |
127 | + width: 34px; | |
128 | + text-align: center; | |
129 | + transition: 0.2s; | |
130 | + } | |
131 | + | |
132 | + .on { | |
133 | + color: #039be5; | |
134 | + } | |
135 | + | |
136 | + .off { | |
137 | + right: 6px; | |
138 | + color: #999; | |
139 | + } | |
140 | + } | |
141 | +</style> | ... | ... |
1 | +import cloneDeep from 'lodash-es/cloneDeep'; | |
2 | +import { ControlComponentSwitchWithIconConfig } from '.'; | |
3 | +import { | |
4 | + ConfigType, | |
5 | + CreateComponentType, | |
6 | + PublicComponentOptions, | |
7 | + PublicPresetOptions, | |
8 | +} from '/@/views/visual/packages/index.type'; | |
9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | |
10 | +import { ComponentConfigFieldEnum } from '../../../enum'; | |
11 | + | |
12 | +export const option: PublicPresetOptions = { | |
13 | + [ComponentConfigFieldEnum.ICON]: 'shuiwen', | |
14 | + [ComponentConfigFieldEnum.ICON_COLOR]: '#377DFF', | |
15 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false, | |
16 | +}; | |
17 | + | |
18 | +export default class Config extends PublicConfigClass implements CreateComponentType { | |
19 | + public key: string = ControlComponentSwitchWithIconConfig.key; | |
20 | + | |
21 | + public attr = { ...componentInitConfig }; | |
22 | + | |
23 | + public componentConfig: ConfigType = cloneDeep(ControlComponentSwitchWithIconConfig); | |
24 | + | |
25 | + public persetOption = cloneDeep(option); | |
26 | + | |
27 | + public option: PublicComponentOptions; | |
28 | + | |
29 | + constructor(option: PublicComponentOptions) { | |
30 | + super(); | |
31 | + this.option = { ...option }; | |
32 | + } | |
33 | +} | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | |
3 | + import { useForm, BasicForm } from '/@/components/Form'; | |
4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
5 | + import { option } from './config'; | |
6 | + | |
7 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | |
8 | + schemas: [ | |
9 | + { | |
10 | + field: ComponentConfigFieldEnum.ICON_COLOR, | |
11 | + label: '图标颜色', | |
12 | + component: 'ColorPicker', | |
13 | + changeEvent: 'update:value', | |
14 | + defaultValue: option.iconColor, | |
15 | + }, | |
16 | + { | |
17 | + field: ComponentConfigFieldEnum.ICON, | |
18 | + label: '图标', | |
19 | + component: 'IconDrawer', | |
20 | + changeEvent: 'update:value', | |
21 | + defaultValue: option.icon, | |
22 | + componentProps({ formModel }) { | |
23 | + const color = formModel[ComponentConfigFieldEnum.ICON_COLOR]; | |
24 | + return { | |
25 | + color, | |
26 | + }; | |
27 | + }, | |
28 | + }, | |
29 | + { | |
30 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
31 | + label: '显示设备名称', | |
32 | + component: 'Checkbox', | |
33 | + defaultValue: option.showDeviceName, | |
34 | + }, | |
35 | + ], | |
36 | + showActionButtonGroup: false, | |
37 | + labelWidth: 120, | |
38 | + baseColProps: { | |
39 | + span: 12, | |
40 | + }, | |
41 | + }); | |
42 | + | |
43 | + const getFormValues = () => { | |
44 | + return getFieldsValue(); | |
45 | + }; | |
46 | + | |
47 | + const setFormValues = (data: Recordable) => { | |
48 | + return setFieldsValue(data); | |
49 | + }; | |
50 | + | |
51 | + defineExpose({ | |
52 | + getFormValues, | |
53 | + setFormValues, | |
54 | + resetFormValues: resetFields, | |
55 | + } as PublicFormInstaceType); | |
56 | +</script> | |
57 | + | |
58 | +<template> | |
59 | + <BasicForm @register="register" /> | |
60 | +</template> | ... | ... |
src/views/visual/packages/components/Control/ControlComponentSwitchWithIcon/datasource.vue
0 → 100644
1 | +<script lang="ts" setup> | |
2 | + import { CreateComponentType } from '/@/views/visual/packages/index.type'; | |
3 | + import { BasicForm, useForm } from '/@/components/Form'; | |
4 | + import { | |
5 | + PublicComponentValueType, | |
6 | + PublicFormInstaceType, | |
7 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
8 | + import { | |
9 | + CommonDataSourceBindValueType, | |
10 | + commonDataSourceSchemas, | |
11 | + } from '../../../config/common.config'; | |
12 | + import { DataSource } from '/@/views/visual/palette/types'; | |
13 | + | |
14 | + defineProps<{ | |
15 | + values: PublicComponentValueType; | |
16 | + componentConfig: CreateComponentType; | |
17 | + }>(); | |
18 | + | |
19 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | |
20 | + labelWidth: 0, | |
21 | + showActionButtonGroup: false, | |
22 | + layout: 'horizontal', | |
23 | + labelCol: { span: 0 }, | |
24 | + schemas: commonDataSourceSchemas(), | |
25 | + }); | |
26 | + | |
27 | + const getFormValues = () => { | |
28 | + let value = getFieldsValue() as CommonDataSourceBindValueType; | |
29 | + value = { | |
30 | + ...value, | |
31 | + customCommand: { | |
32 | + transportType: value.transportType, | |
33 | + service: value.service, | |
34 | + command: value.command, | |
35 | + commandType: value.commandType, | |
36 | + }, | |
37 | + }; | |
38 | + return value; | |
39 | + }; | |
40 | + | |
41 | + const setFormValues = (record: DataSource) => { | |
42 | + const { customCommand } = record; | |
43 | + return setFieldsValue({ | |
44 | + ...record, | |
45 | + transportType: customCommand?.transportType, | |
46 | + service: customCommand?.service, | |
47 | + command: customCommand?.command, | |
48 | + commandType: customCommand?.commandType, | |
49 | + } as unknown as Partial<CommonDataSourceBindValueType>); | |
50 | + }; | |
51 | + | |
52 | + defineExpose({ | |
53 | + getFormValues, | |
54 | + setFormValues, | |
55 | + validate, | |
56 | + resetFormValues: resetFields, | |
57 | + } as PublicFormInstaceType); | |
58 | +</script> | |
59 | + | |
60 | +<template> | |
61 | + <BasicForm @register="register" /> | |
62 | +</template> | ... | ... |
1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | |
2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | |
3 | + | |
4 | +const componentKeys = useComponentKeys('ControlComponentSwitchWithIcon'); | |
5 | + | |
6 | +export const ControlComponentSwitchWithIconConfig: ConfigType = { | |
7 | + ...componentKeys, | |
8 | + title: '控制组件1', | |
9 | + package: PackagesCategoryEnum.CONTROL, | |
10 | +}; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type'; | |
3 | + import { option } from './config'; | |
4 | + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket'; | |
5 | + import { SvgIcon } from '/@/components/Icon'; | |
6 | + import { Switch } from 'ant-design-vue'; | |
7 | + import { computed, ref } from 'vue'; | |
8 | + import { useComponentScale } from '../../../hook/useComponentScale'; | |
9 | + import { useSendCommand } from '../../../hook/useSendCommand'; | |
10 | + import { unref } from 'vue'; | |
11 | + | |
12 | + const props = defineProps<{ | |
13 | + config: ComponentPropsConfigType<typeof option>; | |
14 | + }>(); | |
15 | + | |
16 | + const checked = ref(false); | |
17 | + | |
18 | + const getDesign = computed(() => { | |
19 | + const { option, persetOption } = props.config; | |
20 | + const { componentInfo, attribute, attributeRename } = option; | |
21 | + const { icon: presetIcon, iconColor: presetIconColor } = persetOption || {}; | |
22 | + const { icon, iconColor } = componentInfo || {}; | |
23 | + return { | |
24 | + icon: icon ?? presetIcon, | |
25 | + iconColor: iconColor || presetIconColor, | |
26 | + attribute: attributeRename || attribute, | |
27 | + }; | |
28 | + }); | |
29 | + | |
30 | + const { sendCommand, loading } = useSendCommand(); | |
31 | + const handleChange = async () => { | |
32 | + const flag = await sendCommand(props.config.option, unref(checked)); | |
33 | + if (!flag) checked.value = !unref(checked); | |
34 | + }; | |
35 | + | |
36 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | |
37 | + const { data = {} } = message; | |
38 | + const [latest] = data[attribute] || []; | |
39 | + const [_, value] = latest; | |
40 | + checked.value = !!value; | |
41 | + }; | |
42 | + | |
43 | + useDataFetch(props, updateFn); | |
44 | + const { getScale } = useComponentScale(props); | |
45 | +</script> | |
46 | + | |
47 | +<template> | |
48 | + <main class="w-full h-full flex justify-center items-center" :style="getScale"> | |
49 | + <div class="flex flex-col justify-center items-center mr-20"> | |
50 | + <SvgIcon | |
51 | + :name="getDesign.icon" | |
52 | + prefix="iconfont" | |
53 | + :style="{ color: getDesign.iconColor }" | |
54 | + :size="50" | |
55 | + /> | |
56 | + <span class="mt-3 truncate text-gray-500 text-xs text-center"> | |
57 | + {{ getDesign.attribute || '属性' }} | |
58 | + </span> | |
59 | + </div> | |
60 | + <Switch v-model:checked="checked" :loading="loading" @change="handleChange" /> | |
61 | + </main> | |
62 | +</template> | ... | ... |
1 | +import cloneDeep from 'lodash-es/cloneDeep'; | |
2 | +import { ControlComponentToggleSwitchConfig } from '.'; | |
3 | +import { | |
4 | + ConfigType, | |
5 | + CreateComponentType, | |
6 | + PublicComponentOptions, | |
7 | + PublicPresetOptions, | |
8 | +} from '/@/views/visual/packages/index.type'; | |
9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | |
10 | + | |
11 | +export const option: PublicPresetOptions = { | |
12 | + componetDesign: false, | |
13 | +}; | |
14 | + | |
15 | +export default class Config extends PublicConfigClass implements CreateComponentType { | |
16 | + public key: string = ControlComponentToggleSwitchConfig.key; | |
17 | + | |
18 | + public attr = { ...componentInitConfig }; | |
19 | + | |
20 | + public componentConfig: ConfigType = cloneDeep(ControlComponentToggleSwitchConfig); | |
21 | + | |
22 | + public persetOption = cloneDeep(option); | |
23 | + | |
24 | + public option: PublicComponentOptions; | |
25 | + | |
26 | + constructor(option: PublicComponentOptions) { | |
27 | + super(); | |
28 | + this.option = { ...option }; | |
29 | + } | |
30 | +} | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | |
3 | + import { useForm, BasicForm } from '/@/components/Form'; | |
4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
5 | + | |
6 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | |
7 | + schemas: [ | |
8 | + { | |
9 | + field: ComponentConfigFieldEnum.FONT_COLOR, | |
10 | + label: '数值字体颜色', | |
11 | + component: 'ColorPicker', | |
12 | + changeEvent: 'update:value', | |
13 | + componentProps: { | |
14 | + defaultValue: '#FD7347', | |
15 | + }, | |
16 | + }, | |
17 | + { | |
18 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
19 | + label: '显示设备名称', | |
20 | + component: 'Checkbox', | |
21 | + }, | |
22 | + ], | |
23 | + showActionButtonGroup: false, | |
24 | + labelWidth: 120, | |
25 | + baseColProps: { | |
26 | + span: 12, | |
27 | + }, | |
28 | + }); | |
29 | + | |
30 | + const getFormValues = () => { | |
31 | + return getFieldsValue(); | |
32 | + }; | |
33 | + | |
34 | + const setFormValues = (data: Recordable) => { | |
35 | + return setFieldsValue(data); | |
36 | + }; | |
37 | + | |
38 | + defineExpose({ | |
39 | + getFormValues, | |
40 | + setFormValues, | |
41 | + resetFormValues: resetFields, | |
42 | + } as PublicFormInstaceType); | |
43 | +</script> | |
44 | + | |
45 | +<template> | |
46 | + <BasicForm @register="register" /> | |
47 | +</template> | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { CreateComponentType } from '/@/views/visual/packages/index.type'; | |
3 | + import { BasicForm, useForm } from '/@/components/Form'; | |
4 | + import { | |
5 | + PublicComponentValueType, | |
6 | + PublicFormInstaceType, | |
7 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
8 | + import { | |
9 | + CommonDataSourceBindValueType, | |
10 | + commonDataSourceSchemas, | |
11 | + } from '../../../config/common.config'; | |
12 | + import { DataSource } from '/@/views/visual/palette/types'; | |
13 | + | |
14 | + defineProps<{ | |
15 | + values: PublicComponentValueType; | |
16 | + componentConfig: CreateComponentType; | |
17 | + }>(); | |
18 | + | |
19 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | |
20 | + labelWidth: 0, | |
21 | + showActionButtonGroup: false, | |
22 | + layout: 'horizontal', | |
23 | + labelCol: { span: 0 }, | |
24 | + schemas: commonDataSourceSchemas(), | |
25 | + }); | |
26 | + | |
27 | + const getFormValues = () => { | |
28 | + let value = getFieldsValue() as CommonDataSourceBindValueType; | |
29 | + value = { | |
30 | + ...value, | |
31 | + customCommand: { | |
32 | + transportType: value.transportType, | |
33 | + service: value.service, | |
34 | + command: value.command, | |
35 | + commandType: value.commandType, | |
36 | + }, | |
37 | + }; | |
38 | + return value; | |
39 | + }; | |
40 | + | |
41 | + const setFormValues = (record: DataSource) => { | |
42 | + const { customCommand } = record; | |
43 | + return setFieldsValue({ | |
44 | + ...record, | |
45 | + transportType: customCommand?.transportType, | |
46 | + service: customCommand?.service, | |
47 | + command: customCommand?.command, | |
48 | + commandType: customCommand?.commandType, | |
49 | + } as unknown as Partial<CommonDataSourceBindValueType>); | |
50 | + }; | |
51 | + | |
52 | + defineExpose({ | |
53 | + getFormValues, | |
54 | + setFormValues, | |
55 | + validate, | |
56 | + resetFormValues: resetFields, | |
57 | + } as PublicFormInstaceType); | |
58 | +</script> | |
59 | + | |
60 | +<template> | |
61 | + <BasicForm @register="register" /> | |
62 | +</template> | ... | ... |
1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | |
2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | |
3 | + | |
4 | +const componentKeys = useComponentKeys('ControlComponentToggleSwitch'); | |
5 | + | |
6 | +export const ControlComponentToggleSwitchConfig: ConfigType = { | |
7 | + ...componentKeys, | |
8 | + title: '控制组件3', | |
9 | + package: PackagesCategoryEnum.CONTROL, | |
10 | +}; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type'; | |
3 | + import { option } from './config'; | |
4 | + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket'; | |
5 | + import { Spin } from 'ant-design-vue'; | |
6 | + import { ref } from 'vue'; | |
7 | + import { useComponentScale } from '../../../hook/useComponentScale'; | |
8 | + import { useSendCommand } from '../../../hook/useSendCommand'; | |
9 | + | |
10 | + const props = defineProps<{ | |
11 | + config: ComponentPropsConfigType<typeof option>; | |
12 | + }>(); | |
13 | + | |
14 | + const { getScale } = useComponentScale(props); | |
15 | + | |
16 | + const currentValue = ref(false); | |
17 | + | |
18 | + const { loading, sendCommand } = useSendCommand(); | |
19 | + const handleChange = async (event: Event) => { | |
20 | + const target = event.target as HTMLInputElement; | |
21 | + const value = target.checked; | |
22 | + const flag = await sendCommand(props.config.option, value); | |
23 | + flag ? (currentValue.value = value) : (target.checked = !value); | |
24 | + }; | |
25 | + | |
26 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | |
27 | + const { data = {} } = message; | |
28 | + const [latest] = data[attribute] || []; | |
29 | + const [_, value] = latest; | |
30 | + currentValue.value = !!value; | |
31 | + }; | |
32 | + | |
33 | + useDataFetch(props, updateFn); | |
34 | +</script> | |
35 | + | |
36 | +<template> | |
37 | + <main class="w-full h-full flex flex-col justify-center items-center"> | |
38 | + <Spin :spinning="loading" class="w-full h-full"> | |
39 | + <div class="toggle-switch" :style="getScale"> | |
40 | + <label class="switch"> | |
41 | + <input type="checkbox" :checked="currentValue" @change="handleChange" /> | |
42 | + <div class="button"> | |
43 | + <div class="light"></div> | |
44 | + <div class="dots"></div> | |
45 | + <div class="characters"></div> | |
46 | + <div class="shine"></div> | |
47 | + <div class="shadow"></div> | |
48 | + </div> | |
49 | + </label> | |
50 | + </div> | |
51 | + <div class="text-center mt-2 text-gray-500" :style="getScale">属性</div> | |
52 | + </Spin> | |
53 | + </main> | |
54 | +</template> | |
55 | + | |
56 | +<style scoped> | |
57 | + :deep(.ant-spin-container) { | |
58 | + @apply !flex !flex-col justify-center items-center; | |
59 | + } | |
60 | + | |
61 | + .toggle-switch { | |
62 | + /* flex: 1 1 auto; */ | |
63 | + max-width: 75px; | |
64 | + width: 75px; | |
65 | + max-height: 100px; | |
66 | + height: 100px; | |
67 | + | |
68 | + /* height: 97.5px; */ | |
69 | + display: flex; | |
70 | + } | |
71 | + | |
72 | + .switch { | |
73 | + background-color: black; | |
74 | + box-sizing: border-box; | |
75 | + width: 100%; | |
76 | + height: 100%; | |
77 | + box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.2), 0 0 1px 2px black, inset 0 2px 2px -2px white, | |
78 | + inset 0 0 2px 15px #47434c, inset 0 0 2px 22px black; | |
79 | + border-radius: 5px; | |
80 | + padding: 10px; | |
81 | + perspective: 700px; | |
82 | + } | |
83 | + | |
84 | + .switch input { | |
85 | + display: none; | |
86 | + } | |
87 | + | |
88 | + .switch input:checked + .button { | |
89 | + transform: translateZ(20px) rotateX(25deg); | |
90 | + box-shadow: 0 -5px 10px #ff1818; | |
91 | + } | |
92 | + | |
93 | + .switch input:checked + .button .light { | |
94 | + animation: flicker 0.2s infinite 0.3s; | |
95 | + } | |
96 | + | |
97 | + .switch input:checked + .button .shine { | |
98 | + opacity: 1; | |
99 | + } | |
100 | + | |
101 | + .switch input:checked + .button .shadow { | |
102 | + opacity: 0; | |
103 | + } | |
104 | + | |
105 | + .switch .button { | |
106 | + display: flex; | |
107 | + justify-content: center; | |
108 | + align-items: center; | |
109 | + transition: all 0.3s cubic-bezier(1, 0, 1, 1); | |
110 | + transform-origin: center center -20px; | |
111 | + transform: translateZ(20px) rotateX(-25deg); | |
112 | + transform-style: preserve-3d; | |
113 | + width: 100%; | |
114 | + height: 100%; | |
115 | + position: relative; | |
116 | + cursor: pointer; | |
117 | + background: linear-gradient(#980000 0%, #6f0000 30%, #6f0000 70%, #980000 100%); | |
118 | + background-color: #9b0621; | |
119 | + background-repeat: no-repeat; | |
120 | + } | |
121 | + | |
122 | + .switch .button::before { | |
123 | + content: ''; | |
124 | + background: linear-gradient( | |
125 | + rgba(255, 255, 255, 0.8) 10%, | |
126 | + rgba(255, 255, 255, 0.3) 30%, | |
127 | + #650000 75%, | |
128 | + #320000 | |
129 | + ) | |
130 | + 50% 50%/97% 97%, | |
131 | + #b10000; | |
132 | + background-repeat: no-repeat; | |
133 | + width: 100%; | |
134 | + height: 30px; | |
135 | + transform-origin: top; | |
136 | + transform: rotateX(-90deg); | |
137 | + position: absolute; | |
138 | + top: 0; | |
139 | + } | |
140 | + | |
141 | + .switch .button::after { | |
142 | + content: ''; | |
143 | + background-image: linear-gradient(#650000, #320000); | |
144 | + width: 100%; | |
145 | + height: 30px; | |
146 | + transform-origin: top; | |
147 | + transform: translateY(30px) rotateX(-90deg); | |
148 | + position: absolute; | |
149 | + bottom: 0; | |
150 | + box-shadow: 0 30px 8px 0 black, 0 60px 20px 0 rgb(0 0 0 / 50%); | |
151 | + } | |
152 | + | |
153 | + .switch .light { | |
154 | + opacity: 0; | |
155 | + animation: light-off 1s; | |
156 | + position: absolute; | |
157 | + width: 80%; | |
158 | + height: 80%; | |
159 | + background-image: radial-gradient(#ffc97e, transparent 40%), | |
160 | + radial-gradient(circle, #ff1818 50%, transparent 80%); | |
161 | + } | |
162 | + | |
163 | + .switch .dots { | |
164 | + position: absolute; | |
165 | + width: 100%; | |
166 | + height: 100%; | |
167 | + background-image: radial-gradient(transparent 30%, rgba(101, 0, 0, 0.7) 70%); | |
168 | + background-size: 10px 10px; | |
169 | + } | |
170 | + | |
171 | + .switch .characters { | |
172 | + position: absolute; | |
173 | + width: 100%; | |
174 | + height: 100%; | |
175 | + background: linear-gradient(white, white) 50% 20%/5% 20%, | |
176 | + radial-gradient(circle, transparent 50%, white 52%, white 70%, transparent 72%) 50% 80%/33% | |
177 | + 25%; | |
178 | + background-repeat: no-repeat; | |
179 | + } | |
180 | + | |
181 | + .switch .shine { | |
182 | + transition: all 0.3s cubic-bezier(1, 0, 1, 1); | |
183 | + opacity: 0.3; | |
184 | + position: absolute; | |
185 | + width: 100%; | |
186 | + height: 100%; | |
187 | + background: linear-gradient(white, transparent 3%) 50% 50%/97% 97%, | |
188 | + linear-gradient( | |
189 | + rgba(255, 255, 255, 0.5), | |
190 | + transparent 50%, | |
191 | + transparent 80%, | |
192 | + rgba(255, 255, 255, 0.5) | |
193 | + ) | |
194 | + 50% 50%/97% 97%; | |
195 | + background-repeat: no-repeat; | |
196 | + } | |
197 | + | |
198 | + .switch .shadow { | |
199 | + transition: all 0.3s cubic-bezier(1, 0, 1, 1); | |
200 | + opacity: 1; | |
201 | + position: absolute; | |
202 | + width: 100%; | |
203 | + height: 100%; | |
204 | + background: linear-gradient(transparent 70%, rgba(0, 0, 0, 0.8)); | |
205 | + background-repeat: no-repeat; | |
206 | + } | |
207 | + | |
208 | + @keyframes flicker { | |
209 | + 0% { | |
210 | + opacity: 1; | |
211 | + } | |
212 | + | |
213 | + 80% { | |
214 | + opacity: 0.8; | |
215 | + } | |
216 | + | |
217 | + 100% { | |
218 | + opacity: 1; | |
219 | + } | |
220 | + } | |
221 | + | |
222 | + @keyframes light-off { | |
223 | + 0% { | |
224 | + opacity: 1; | |
225 | + } | |
226 | + | |
227 | + 80% { | |
228 | + opacity: 0; | |
229 | + } | |
230 | + } | |
231 | +</style> | ... | ... |
1 | +import { ControlComponentSlidingSwitchConfig } from './ControlComponentSlidingSwitch'; | |
2 | +import { ControlComponentSwitchWithIconConfig } from './ControlComponentSwitchWithIcon'; | |
3 | +import { ControlComponentToggleSwitchConfig } from './ControlComponentToggleSwitch'; | |
4 | + | |
5 | +export const ControlList = [ | |
6 | + ControlComponentSwitchWithIconConfig, | |
7 | + ControlComponentSlidingSwitchConfig, | |
8 | + ControlComponentToggleSwitchConfig, | |
9 | +]; | ... | ... |
1 | +import cloneDeep from 'lodash-es/cloneDeep'; | |
2 | +import { DigitalDashboardComponentConfig } from '.'; | |
3 | +import { | |
4 | + ConfigType, | |
5 | + CreateComponentType, | |
6 | + PublicComponentOptions, | |
7 | + PublicPresetOptions, | |
8 | +} from '/@/views/visual/packages/index.type'; | |
9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | |
10 | +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | |
11 | + | |
12 | +export const option: PublicPresetOptions = { | |
13 | + [ComponentConfigFieldEnum.FONT_COLOR]: '#000', | |
14 | + [ComponentConfigFieldEnum.UNIT]: 'kw/h', | |
15 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false, | |
16 | +}; | |
17 | + | |
18 | +export default class Config extends PublicConfigClass implements CreateComponentType { | |
19 | + public key: string = DigitalDashboardComponentConfig.key; | |
20 | + | |
21 | + public attr = { ...componentInitConfig, w: 340 }; | |
22 | + | |
23 | + public componentConfig: ConfigType = cloneDeep(DigitalDashboardComponentConfig); | |
24 | + | |
25 | + public persetOption = cloneDeep(option); | |
26 | + | |
27 | + public option: PublicComponentOptions; | |
28 | + | |
29 | + constructor(option: PublicComponentOptions) { | |
30 | + super(); | |
31 | + this.option = { ...option }; | |
32 | + } | |
33 | +} | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | |
3 | + import { useForm, BasicForm } from '/@/components/Form'; | |
4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
5 | + import { option } from './config'; | |
6 | + | |
7 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | |
8 | + schemas: [ | |
9 | + { | |
10 | + field: ComponentConfigFieldEnum.FONT_COLOR, | |
11 | + label: '数值字体颜色', | |
12 | + component: 'ColorPicker', | |
13 | + changeEvent: 'update:value', | |
14 | + defaultValue: option.fontColor, | |
15 | + }, | |
16 | + { | |
17 | + field: ComponentConfigFieldEnum.UNIT, | |
18 | + label: '数值单位', | |
19 | + component: 'Input', | |
20 | + defaultValue: option.unit, | |
21 | + componentProps: { | |
22 | + placeholder: '请输入数值单位', | |
23 | + }, | |
24 | + }, | |
25 | + { | |
26 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
27 | + label: '显示设备名称', | |
28 | + component: 'Checkbox', | |
29 | + defaultValue: option.showDeviceName, | |
30 | + }, | |
31 | + ], | |
32 | + showActionButtonGroup: false, | |
33 | + labelWidth: 120, | |
34 | + baseColProps: { | |
35 | + span: 12, | |
36 | + }, | |
37 | + }); | |
38 | + | |
39 | + const getFormValues = () => { | |
40 | + return getFieldsValue(); | |
41 | + }; | |
42 | + | |
43 | + const setFormValues = (data: Recordable) => { | |
44 | + return setFieldsValue(data); | |
45 | + }; | |
46 | + | |
47 | + defineExpose({ | |
48 | + getFormValues, | |
49 | + setFormValues, | |
50 | + resetFormValues: resetFields, | |
51 | + } as PublicFormInstaceType); | |
52 | +</script> | |
53 | + | |
54 | +<template> | |
55 | + <BasicForm @register="register" /> | |
56 | +</template> | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { CreateComponentType } from '/@/views/visual/packages/index.type'; | |
3 | + import { BasicForm, useForm } from '/@/components/Form'; | |
4 | + import { | |
5 | + PublicComponentValueType, | |
6 | + PublicFormInstaceType, | |
7 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
8 | + import { commonDataSourceSchemas } from '../../../config/common.config'; | |
9 | + | |
10 | + defineProps<{ | |
11 | + values: PublicComponentValueType; | |
12 | + componentConfig: CreateComponentType; | |
13 | + }>(); | |
14 | + | |
15 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | |
16 | + labelWidth: 0, | |
17 | + showActionButtonGroup: false, | |
18 | + layout: 'horizontal', | |
19 | + labelCol: { span: 0 }, | |
20 | + schemas: commonDataSourceSchemas(), | |
21 | + }); | |
22 | + | |
23 | + const getFormValues = () => { | |
24 | + return getFieldsValue(); | |
25 | + }; | |
26 | + | |
27 | + const setFormValues = (record: Recordable) => { | |
28 | + return setFieldsValue(record); | |
29 | + }; | |
30 | + | |
31 | + defineExpose({ | |
32 | + getFormValues, | |
33 | + setFormValues, | |
34 | + validate, | |
35 | + resetFormValues: resetFields, | |
36 | + } as PublicFormInstaceType); | |
37 | +</script> | |
38 | + | |
39 | +<template> | |
40 | + <BasicForm @register="register" /> | |
41 | +</template> | ... | ... |
1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | |
2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | |
3 | + | |
4 | +const componentKeys = useComponentKeys('DigitalDashboardComponent'); | |
5 | + | |
6 | +export const DigitalDashboardComponentConfig: ConfigType = { | |
7 | + ...componentKeys, | |
8 | + title: '数字仪表盘', | |
9 | + package: PackagesCategoryEnum.INSTRUMENT, | |
10 | +}; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type'; | |
3 | + import { option } from './config'; | |
4 | + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket'; | |
5 | + import { ref, computed, unref } from 'vue'; | |
6 | + import { Space } from 'ant-design-vue'; | |
7 | + import { useComponentScale } from '/@/views/visual/packages/hook/useComponentScale'; | |
8 | + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime'; | |
9 | + | |
10 | + const props = defineProps<{ | |
11 | + config: ComponentPropsConfigType<typeof option>; | |
12 | + }>(); | |
13 | + | |
14 | + const currentValue = ref(99.23); | |
15 | + | |
16 | + const time = ref<Nullable<number>>(null); | |
17 | + | |
18 | + const integerPart = computed(() => { | |
19 | + let number = unref(currentValue); | |
20 | + const max = 5; | |
21 | + if (isNaN(number)) number = 0; | |
22 | + let _value = number.toString().split('.')[0]; | |
23 | + | |
24 | + if (_value.length > max) return ''.padStart(max, '9'); | |
25 | + if (_value.length < max) return _value.padStart(max, '0'); | |
26 | + | |
27 | + return _value; | |
28 | + }); | |
29 | + | |
30 | + const decimalPart = computed(() => { | |
31 | + let number = unref(currentValue); | |
32 | + | |
33 | + const keepNumber = 2; | |
34 | + | |
35 | + if (isNaN(number)) number = 0; | |
36 | + | |
37 | + let _value = number.toString().split('.')[1] || '0'; | |
38 | + | |
39 | + if (_value.length < keepNumber) return ''.padStart(keepNumber, '0'); | |
40 | + | |
41 | + if (_value.length > keepNumber) return _value.slice(0, 2); | |
42 | + | |
43 | + return _value; | |
44 | + }); | |
45 | + | |
46 | + const getDesign = computed(() => { | |
47 | + const { option, persetOption } = props.config; | |
48 | + const { componentInfo, attribute, attributeRename } = option; | |
49 | + const { fontColor: presetFontColor, unit: presetUnit } = persetOption || {}; | |
50 | + const { unit, fontColor } = componentInfo || {}; | |
51 | + return { | |
52 | + unit: unit ?? presetUnit, | |
53 | + fontColor: fontColor ?? presetFontColor, | |
54 | + attribute: attributeRename || attribute, | |
55 | + }; | |
56 | + }); | |
57 | + | |
58 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | |
59 | + const { data = {} } = message; | |
60 | + const [latest] = data[attribute] || []; | |
61 | + const [timespan, value] = latest; | |
62 | + time.value = timespan; | |
63 | + currentValue.value = isNaN(value as unknown as number) ? 0 : Number(value); | |
64 | + }; | |
65 | + | |
66 | + useDataFetch(props, updateFn); | |
67 | + | |
68 | + const { getScale } = useComponentScale(props); | |
69 | +</script> | |
70 | + | |
71 | +<template> | |
72 | + <main class="w-full h-full flex flex-col justify-center items-center"> | |
73 | + <div class="flex flex-col w-full h-full"> | |
74 | + <div class="flex-1 flex justify-center items-center"> | |
75 | + <div class="flex px-4 items-center transform scale-75" :style="getScale"> | |
76 | + <Space | |
77 | + justify="end" | |
78 | + class="justify-end" | |
79 | + :size="4" | |
80 | + :style="{ | |
81 | + backgroundColor: '#585357', | |
82 | + padding: '10px', | |
83 | + }" | |
84 | + > | |
85 | + <div | |
86 | + v-for="number in integerPart" | |
87 | + :key="number" | |
88 | + class="digital-wrapper__int" | |
89 | + :style="{ | |
90 | + color: getDesign.fontColor, | |
91 | + }" | |
92 | + > | |
93 | + <div class="digital-text__int p-1 text-light-50"> {{ number }}</div> | |
94 | + </div> | |
95 | + </Space> | |
96 | + <div | |
97 | + class="m-0.5 rounded-1/2" | |
98 | + style="background-color: #333; width: 6px; height: 6px; align-self: flex-end" | |
99 | + > | |
100 | + </div> | |
101 | + <Space | |
102 | + justify="end" | |
103 | + class="justify-end" | |
104 | + :size="4" | |
105 | + :style="{ | |
106 | + backgroundColor: '#b74940', | |
107 | + padding: '10px', | |
108 | + }" | |
109 | + > | |
110 | + <div | |
111 | + v-for="number in decimalPart" | |
112 | + :key="number" | |
113 | + class="digital-wrapper__float" | |
114 | + :style="{ | |
115 | + color: getDesign.fontColor, | |
116 | + }" | |
117 | + > | |
118 | + <div class="digital-text__float p-1 text-light-50"> | |
119 | + {{ number }} | |
120 | + </div> | |
121 | + </div> | |
122 | + </Space> | |
123 | + <div class="px-1 font-bold"> | |
124 | + {{ getDesign.unit || 'kw/h' }} | |
125 | + </div> | |
126 | + </div> | |
127 | + </div> | |
128 | + | |
129 | + <div class="text-center truncate text-xs text-gray-500"> | |
130 | + <span>{{ getDesign.attribute || '电表' }}</span> | |
131 | + </div> | |
132 | + | |
133 | + <UpdateTime :time="time" /> | |
134 | + </div> | |
135 | + </main> | |
136 | +</template> | |
137 | + | |
138 | +<style scoped lang="less"> | |
139 | + .digital-wrapper__int { | |
140 | + border-radius: 1px; | |
141 | + box-shadow: inset 0 1px 3px 0 rgba(0, 0, 0, 0.7); | |
142 | + background: url('/@/assets/images/digital-wrapper-bg-int.png') 0 -1px no-repeat; | |
143 | + padding: 5px; | |
144 | + background-size: 100% 100%; | |
145 | + } | |
146 | + | |
147 | + .digital-text_int { | |
148 | + display: inline-block; | |
149 | + overflow-wrap: break-word; | |
150 | + color: rgba(255, 255, 255, 1); | |
151 | + white-space: nowrap; | |
152 | + text-align: center; | |
153 | + } | |
154 | + | |
155 | + .digital-wrapper__float { | |
156 | + border-radius: 1px; | |
157 | + box-shadow: inset 0 1px 3px 0 rgba(112, 22, 15, 1); | |
158 | + background: url('/@/assets/images/digital-wrapper-bg-float.png') 0 -1px no-repeat; | |
159 | + padding: 5px; | |
160 | + background-size: 100% 100%; | |
161 | + } | |
162 | + | |
163 | + .digital-text_float { | |
164 | + display: inline-block; | |
165 | + overflow-wrap: break-word; | |
166 | + color: rgba(255, 255, 255, 1); | |
167 | + white-space: nowrap; | |
168 | + text-align: center; | |
169 | + } | |
170 | +</style> | ... | ... |
1 | +import cloneDeep from 'lodash-es/cloneDeep'; | |
2 | +import { InstrumentComponent1Config } from '.'; | |
3 | +import { | |
4 | + ConfigType, | |
5 | + CreateComponentType, | |
6 | + PublicComponentOptions, | |
7 | + PublicPresetOptions, | |
8 | +} from '../../../index.type'; | |
9 | +import { PublicConfigClass, componentInitConfig } from '../../../publicConfig'; | |
10 | +import { ComponentConfigFieldEnum } from '../../../enum'; | |
11 | + | |
12 | +export const option: PublicPresetOptions = { | |
13 | + [ComponentConfigFieldEnum.FONT_COLOR]: '#FD7347', | |
14 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false, | |
15 | + [ComponentConfigFieldEnum.UNIT]: '℃', | |
16 | +}; | |
17 | + | |
18 | +export default class Config extends PublicConfigClass implements CreateComponentType { | |
19 | + public key: string = InstrumentComponent1Config.key; | |
20 | + | |
21 | + public attr = { ...componentInitConfig }; | |
22 | + | |
23 | + public componentConfig: ConfigType = cloneDeep(InstrumentComponent1Config); | |
24 | + | |
25 | + public persetOption = cloneDeep(option); | |
26 | + | |
27 | + public option: PublicComponentOptions; | |
28 | + | |
29 | + constructor(option: PublicComponentOptions) { | |
30 | + super(); | |
31 | + this.option = { ...option }; | |
32 | + } | |
33 | +} | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ComponentConfigFieldEnum } from '../../../enum'; | |
3 | + import { option } from './config'; | |
4 | + import { useForm, BasicForm } from '/@/components/Form'; | |
5 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
6 | + | |
7 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | |
8 | + schemas: [ | |
9 | + { | |
10 | + field: ComponentConfigFieldEnum.FONT_COLOR, | |
11 | + label: '数值字体颜色', | |
12 | + component: 'ColorPicker', | |
13 | + changeEvent: 'update:value', | |
14 | + defaultValue: option.fontColor, | |
15 | + }, | |
16 | + { | |
17 | + field: ComponentConfigFieldEnum.UNIT, | |
18 | + label: '数值单位', | |
19 | + component: 'Input', | |
20 | + defaultValue: option.unit, | |
21 | + }, | |
22 | + { | |
23 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
24 | + label: '显示设备名称', | |
25 | + component: 'Checkbox', | |
26 | + defaultValue: option.showDeviceName, | |
27 | + }, | |
28 | + ], | |
29 | + showActionButtonGroup: false, | |
30 | + labelWidth: 120, | |
31 | + baseColProps: { | |
32 | + span: 12, | |
33 | + }, | |
34 | + }); | |
35 | + | |
36 | + const getFormValues = () => { | |
37 | + return getFieldsValue(); | |
38 | + }; | |
39 | + | |
40 | + const setFormValues = (data: Recordable) => { | |
41 | + return setFieldsValue(data); | |
42 | + }; | |
43 | + | |
44 | + defineExpose({ | |
45 | + getFormValues, | |
46 | + setFormValues, | |
47 | + resetFormValues: resetFields, | |
48 | + } as PublicFormInstaceType); | |
49 | +</script> | |
50 | + | |
51 | +<template> | |
52 | + <BasicForm @register="register" /> | |
53 | +</template> | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { commonDataSourceSchemas } from '../../../config/common.config'; | |
3 | + import { CreateComponentType } from '../../../index.type'; | |
4 | + import { BasicForm, useForm } from '/@/components/Form'; | |
5 | + import { | |
6 | + PublicComponentValueType, | |
7 | + PublicFormInstaceType, | |
8 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
9 | + | |
10 | + defineProps<{ | |
11 | + values: PublicComponentValueType; | |
12 | + componentConfig: CreateComponentType; | |
13 | + }>(); | |
14 | + | |
15 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | |
16 | + labelWidth: 0, | |
17 | + showActionButtonGroup: false, | |
18 | + layout: 'horizontal', | |
19 | + labelCol: { span: 0 }, | |
20 | + schemas: commonDataSourceSchemas(), | |
21 | + }); | |
22 | + | |
23 | + const getFormValues = () => { | |
24 | + return getFieldsValue(); | |
25 | + }; | |
26 | + | |
27 | + const setFormValues = (record: Recordable) => { | |
28 | + return setFieldsValue(record); | |
29 | + }; | |
30 | + | |
31 | + defineExpose({ | |
32 | + getFormValues, | |
33 | + setFormValues, | |
34 | + validate, | |
35 | + resetFormValues: resetFields, | |
36 | + } as PublicFormInstaceType); | |
37 | +</script> | |
38 | + | |
39 | +<template> | |
40 | + <BasicForm @register="register" /> | |
41 | +</template> | ... | ... |
1 | +import { useComponentKeys } from '../../../hook/useComponentKeys'; | |
2 | +import { ConfigType, PackagesCategoryEnum } from '../../../index.type'; | |
3 | + | |
4 | +const componentKeys = useComponentKeys('InstrumentComponent1'); | |
5 | + | |
6 | +export const InstrumentComponent1Config: ConfigType = { | |
7 | + ...componentKeys, | |
8 | + title: '仪表盘', | |
9 | + package: PackagesCategoryEnum.INSTRUMENT, | |
10 | +}; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { EChartsOption, ECharts, init } from 'echarts'; | |
3 | + import { onMounted } from 'vue'; | |
4 | + import { unref } from 'vue'; | |
5 | + import { ref } from 'vue'; | |
6 | + import { useDataFetch } from '../../../hook/useSocket'; | |
7 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '../../../index.type'; | |
8 | + import { option } from './config'; | |
9 | + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime'; | |
10 | + import { useIntervalFn } from '@vueuse/core'; | |
11 | + import { computed } from 'vue'; | |
12 | + import { useComponentScale } from '../../../hook/useComponentScale'; | |
13 | + import { nextTick } from 'vue'; | |
14 | + import { DeviceName } from '/@/views/visual/commonComponents/DeviceName'; | |
15 | + | |
16 | + const props = defineProps<{ | |
17 | + config: ComponentPropsConfigType<typeof option>; | |
18 | + }>(); | |
19 | + | |
20 | + const chartRefEl = ref<Nullable<HTMLDivElement>>(null); | |
21 | + | |
22 | + const chartInstance = ref<Nullable<ECharts>>(null); | |
23 | + | |
24 | + const time = ref<Nullable<number>>(null); | |
25 | + | |
26 | + const getDesign = computed(() => { | |
27 | + const { option, persetOption } = props.config; | |
28 | + const { componentInfo, attribute, attributeRename } = option; | |
29 | + const { fontColor: presetFontColor, unit: presetUnit } = persetOption || {}; | |
30 | + const { unit, fontColor } = componentInfo || {}; | |
31 | + return { | |
32 | + unit: unit ?? presetUnit, | |
33 | + fontColor: fontColor ?? presetFontColor, | |
34 | + attribute: attributeRename || attribute, | |
35 | + }; | |
36 | + }); | |
37 | + | |
38 | + const options = (): EChartsOption => { | |
39 | + const { unit, fontColor } = unref(getDesign); | |
40 | + return { | |
41 | + series: [ | |
42 | + { | |
43 | + type: 'gauge', | |
44 | + radius: '50%', | |
45 | + center: ['50%', '60%'], | |
46 | + startAngle: 200, | |
47 | + endAngle: -20, | |
48 | + min: 0, | |
49 | + max: 100, | |
50 | + splitNumber: 10, | |
51 | + itemStyle: { | |
52 | + color: fontColor, | |
53 | + }, | |
54 | + progress: { | |
55 | + show: true, | |
56 | + width: 30, | |
57 | + }, | |
58 | + pointer: { | |
59 | + show: false, | |
60 | + }, | |
61 | + axisLine: { | |
62 | + lineStyle: { | |
63 | + width: 30, | |
64 | + }, | |
65 | + }, | |
66 | + axisTick: { | |
67 | + distance: -35, | |
68 | + splitNumber: 5, | |
69 | + lineStyle: { | |
70 | + width: 2, | |
71 | + color: '#999', | |
72 | + }, | |
73 | + }, | |
74 | + splitLine: { | |
75 | + distance: -40, | |
76 | + length: 10, | |
77 | + lineStyle: { | |
78 | + width: 3, | |
79 | + color: '#999', | |
80 | + }, | |
81 | + }, | |
82 | + axisLabel: { | |
83 | + distance: 0, | |
84 | + color: '#999', | |
85 | + }, | |
86 | + anchor: { | |
87 | + show: false, | |
88 | + }, | |
89 | + title: { | |
90 | + show: false, | |
91 | + }, | |
92 | + detail: { | |
93 | + valueAnimation: true, | |
94 | + width: '60%', | |
95 | + lineHeight: 10, | |
96 | + borderRadius: 8, | |
97 | + offsetCenter: [0, '30%'], | |
98 | + fontSize: 14, | |
99 | + fontWeight: 'bolder', | |
100 | + formatter: `{value} ${unit ?? ''}`, | |
101 | + color: fontColor || 'inherit', | |
102 | + }, | |
103 | + data: [ | |
104 | + { | |
105 | + value: 20, | |
106 | + }, | |
107 | + ], | |
108 | + }, | |
109 | + { | |
110 | + type: 'gauge', | |
111 | + radius: '50%', | |
112 | + center: ['50%', '60%'], | |
113 | + startAngle: 200, | |
114 | + endAngle: -20, | |
115 | + min: 0, | |
116 | + max: 100, | |
117 | + itemStyle: { | |
118 | + color: fontColor, | |
119 | + }, | |
120 | + progress: { | |
121 | + show: true, | |
122 | + width: 8, | |
123 | + }, | |
124 | + pointer: { | |
125 | + show: false, | |
126 | + }, | |
127 | + axisLine: { | |
128 | + show: false, | |
129 | + }, | |
130 | + axisTick: { | |
131 | + show: false, | |
132 | + }, | |
133 | + splitLine: { | |
134 | + show: false, | |
135 | + }, | |
136 | + axisLabel: { | |
137 | + show: false, | |
138 | + }, | |
139 | + detail: { | |
140 | + show: false, | |
141 | + }, | |
142 | + data: [ | |
143 | + { | |
144 | + value: 20, | |
145 | + }, | |
146 | + ], | |
147 | + }, | |
148 | + ], | |
149 | + }; | |
150 | + }; | |
151 | + | |
152 | + const updateChart = (value: number) => { | |
153 | + unref(chartInstance)?.setOption({ | |
154 | + series: [{ data: [{ value: value.toFixed(2) }] }, { data: [{ value: value.toFixed(2) }] }], | |
155 | + } as EChartsOption); | |
156 | + }; | |
157 | + | |
158 | + const initial = () => { | |
159 | + chartInstance.value = init(unref(chartRefEl)!); | |
160 | + chartInstance.value.setOption(options()); | |
161 | + }; | |
162 | + | |
163 | + const randomFn = () => { | |
164 | + useIntervalFn(() => { | |
165 | + const value = (Math.random() * 100).toFixed(0); | |
166 | + unref(chartInstance)?.setOption({ | |
167 | + series: [{ data: [{ value }] }, { data: [{ value }] }], | |
168 | + } as EChartsOption); | |
169 | + }, 3000); | |
170 | + }; | |
171 | + | |
172 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | |
173 | + const { data = {} } = message; | |
174 | + const [latest] = data[attribute] || []; | |
175 | + const [timespan, value] = latest; | |
176 | + time.value = timespan; | |
177 | + updateChart(isNaN(value as unknown as number) ? 0 : Number(value)); | |
178 | + }; | |
179 | + | |
180 | + useDataFetch(props, updateFn); | |
181 | + | |
182 | + onMounted(() => { | |
183 | + initial(); | |
184 | + !props.config.option.uuid && randomFn(); | |
185 | + }); | |
186 | + | |
187 | + const resize = async () => { | |
188 | + await nextTick(); | |
189 | + unref(chartInstance)?.resize(); | |
190 | + }; | |
191 | + | |
192 | + useComponentScale(props, resize); | |
193 | +</script> | |
194 | + | |
195 | +<template> | |
196 | + <main class="w-full h-full flex flex-col justify-center items-center"> | |
197 | + <DeviceName :config="config" /> | |
198 | + <div ref="chartRefEl" class="flex-1 w-full h-full"> </div> | |
199 | + <div class="text-gray-500 text-xs text-center truncate">{{ | |
200 | + getDesign.attribute || '温度' | |
201 | + }}</div> | |
202 | + <UpdateTime :time="time" /> | |
203 | + </main> | |
204 | +</template> | ... | ... |
1 | +import cloneDeep from 'lodash-es/cloneDeep'; | |
2 | +import { InstrumentComponent2Config } from '.'; | |
3 | +import { | |
4 | + ConfigType, | |
5 | + CreateComponentType, | |
6 | + PublicComponentOptions, | |
7 | + PublicPresetOptions, | |
8 | +} from '/@/views/visual/packages/index.type'; | |
9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | |
10 | +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | |
11 | + | |
12 | +export enum Gradient { | |
13 | + FIRST = 'first', | |
14 | + SECOND = 'second', | |
15 | + THIRD = 'third', | |
16 | +} | |
17 | +export enum GradientColor { | |
18 | + FIRST = '#67e0e3', | |
19 | + SECOND = '#37a2da', | |
20 | + THIRD = '#fd666d', | |
21 | +} | |
22 | + | |
23 | +export const option: PublicPresetOptions = { | |
24 | + [ComponentConfigFieldEnum.FONT_COLOR]: GradientColor.THIRD, | |
25 | + [ComponentConfigFieldEnum.GRADIENT_INFO]: [ | |
26 | + { key: Gradient.FIRST, value: 30, color: GradientColor.FIRST }, | |
27 | + { key: Gradient.SECOND, value: 70, color: GradientColor.SECOND }, | |
28 | + { key: Gradient.THIRD, value: 100, color: GradientColor.THIRD }, | |
29 | + ], | |
30 | + [ComponentConfigFieldEnum.UNIT]: 'km/h', | |
31 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false, | |
32 | +}; | |
33 | + | |
34 | +export default class Config extends PublicConfigClass implements CreateComponentType { | |
35 | + public key: string = InstrumentComponent2Config.key; | |
36 | + | |
37 | + public attr = { ...componentInitConfig }; | |
38 | + | |
39 | + public componentConfig: ConfigType = cloneDeep(InstrumentComponent2Config); | |
40 | + | |
41 | + public persetOption = cloneDeep(option); | |
42 | + | |
43 | + public option: PublicComponentOptions; | |
44 | + | |
45 | + constructor(option: PublicComponentOptions) { | |
46 | + super(); | |
47 | + this.option = { ...option }; | |
48 | + } | |
49 | +} | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | |
3 | + import { useForm, BasicForm } from '/@/components/Form'; | |
4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
5 | + import { Gradient, GradientColor, option } from './config'; | |
6 | + import { ComponentInfo } from '/@/views/visual/palette/types'; | |
7 | + | |
8 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | |
9 | + schemas: [ | |
10 | + { | |
11 | + field: ComponentConfigFieldEnum.FONT_COLOR, | |
12 | + label: '数值字体颜色', | |
13 | + component: 'ColorPicker', | |
14 | + changeEvent: 'update:value', | |
15 | + defaultValue: option.fontColor, | |
16 | + }, | |
17 | + { | |
18 | + field: ComponentConfigFieldEnum.UNIT, | |
19 | + label: '数值单位', | |
20 | + component: 'Input', | |
21 | + defaultValue: option.unit, | |
22 | + componentProps: { | |
23 | + placeholder: '请输入数值单位', | |
24 | + }, | |
25 | + }, | |
26 | + { | |
27 | + field: ComponentConfigFieldEnum.FIRST_PHASE_COLOR, | |
28 | + label: '一阶段颜色', | |
29 | + component: 'ColorPicker', | |
30 | + changeEvent: 'update:value', | |
31 | + defaultValue: GradientColor.FIRST, | |
32 | + }, | |
33 | + { | |
34 | + field: ComponentConfigFieldEnum.FIRST_PHASE_VALUE, | |
35 | + label: '一阶段阀值', | |
36 | + component: 'InputNumber', | |
37 | + componentProps: { | |
38 | + placeholder: '请输入一阶段阀值', | |
39 | + min: 0, | |
40 | + }, | |
41 | + }, | |
42 | + { | |
43 | + field: ComponentConfigFieldEnum.SECOND_PHASE_COLOR, | |
44 | + label: '二阶段颜色', | |
45 | + component: 'ColorPicker', | |
46 | + changeEvent: 'update:value', | |
47 | + defaultValue: GradientColor.SECOND, | |
48 | + }, | |
49 | + { | |
50 | + field: ComponentConfigFieldEnum.SECOND_PHASE_VALUE, | |
51 | + label: '二阶段阀值', | |
52 | + component: 'InputNumber', | |
53 | + componentProps: ({ formModel }) => { | |
54 | + return { | |
55 | + placeholder: '请输入二阶段阀值', | |
56 | + min: formModel[ComponentConfigFieldEnum.FIRST_PHASE_VALUE], | |
57 | + }; | |
58 | + }, | |
59 | + }, | |
60 | + { | |
61 | + field: ComponentConfigFieldEnum.THIRD_PHASE_COLOR, | |
62 | + label: '三阶段颜色', | |
63 | + component: 'ColorPicker', | |
64 | + changeEvent: 'update:value', | |
65 | + defaultValue: GradientColor.THIRD, | |
66 | + }, | |
67 | + { | |
68 | + field: ComponentConfigFieldEnum.THIRD_PHASE_VALUE, | |
69 | + label: '三阶段阀值', | |
70 | + component: 'InputNumber', | |
71 | + componentProps: ({ formModel }) => { | |
72 | + return { | |
73 | + placeholder: '请输入三阶段阀值', | |
74 | + min: formModel[ComponentConfigFieldEnum.SECOND_PHASE_VALUE], | |
75 | + }; | |
76 | + }, | |
77 | + }, | |
78 | + { | |
79 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
80 | + label: '显示设备名称', | |
81 | + component: 'Checkbox', | |
82 | + defaultValue: option.showDeviceName, | |
83 | + }, | |
84 | + ], | |
85 | + showActionButtonGroup: false, | |
86 | + labelWidth: 120, | |
87 | + baseColProps: { | |
88 | + span: 12, | |
89 | + }, | |
90 | + }); | |
91 | + | |
92 | + const getFormValues = () => { | |
93 | + const value = getFieldsValue(); | |
94 | + return { | |
95 | + gradientInfo: [ | |
96 | + { | |
97 | + key: Gradient.FIRST, | |
98 | + value: value[ComponentConfigFieldEnum.FIRST_PHASE_VALUE], | |
99 | + color: value[ComponentConfigFieldEnum.FIRST_PHASE_COLOR], | |
100 | + }, | |
101 | + { | |
102 | + key: Gradient.SECOND, | |
103 | + value: value[ComponentConfigFieldEnum.SECOND_PHASE_VALUE], | |
104 | + color: value[ComponentConfigFieldEnum.SECOND_PHASE_COLOR], | |
105 | + }, | |
106 | + { | |
107 | + key: Gradient.THIRD, | |
108 | + value: value[ComponentConfigFieldEnum.THIRD_PHASE_VALUE], | |
109 | + color: value[ComponentConfigFieldEnum.THIRD_PHASE_COLOR], | |
110 | + }, | |
111 | + ], | |
112 | + fontColor: value[ComponentConfigFieldEnum.FONT_COLOR], | |
113 | + unit: value[ComponentConfigFieldEnum.UNIT], | |
114 | + showDeviceName: value[ComponentConfigFieldEnum.SHOW_DEVICE_NAME], | |
115 | + } as ComponentInfo; | |
116 | + }; | |
117 | + | |
118 | + const setFormValues = (data: ComponentInfo) => { | |
119 | + const { gradientInfo, unit, fontColor, showDeviceName } = data; | |
120 | + const firstRecord = gradientInfo.find((item) => item.key === Gradient.FIRST); | |
121 | + const secondRecord = gradientInfo.find((item) => item.key === Gradient.SECOND); | |
122 | + const thirdRecord = gradientInfo.find((item) => item.key === Gradient.THIRD); | |
123 | + const value = { | |
124 | + [ComponentConfigFieldEnum.UNIT]: unit, | |
125 | + [ComponentConfigFieldEnum.FONT_COLOR]: fontColor, | |
126 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: showDeviceName, | |
127 | + [ComponentConfigFieldEnum.FIRST_PHASE_VALUE]: firstRecord?.value, | |
128 | + [ComponentConfigFieldEnum.FIRST_PHASE_COLOR]: firstRecord?.color, | |
129 | + [ComponentConfigFieldEnum.SECOND_PHASE_VALUE]: secondRecord?.value, | |
130 | + [ComponentConfigFieldEnum.SECOND_PHASE_COLOR]: secondRecord?.color, | |
131 | + [ComponentConfigFieldEnum.THIRD_PHASE_VALUE]: thirdRecord?.value, | |
132 | + [ComponentConfigFieldEnum.THIRD_PHASE_COLOR]: thirdRecord?.color, | |
133 | + }; | |
134 | + return setFieldsValue(value); | |
135 | + }; | |
136 | + | |
137 | + defineExpose({ | |
138 | + getFormValues, | |
139 | + setFormValues, | |
140 | + resetFormValues: resetFields, | |
141 | + } as PublicFormInstaceType); | |
142 | +</script> | |
143 | + | |
144 | +<template> | |
145 | + <BasicForm @register="register" /> | |
146 | +</template> | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { CreateComponentType } from '/@/views/visual/packages/index.type'; | |
3 | + import { BasicForm, useForm } from '/@/components/Form'; | |
4 | + import { | |
5 | + PublicComponentValueType, | |
6 | + PublicFormInstaceType, | |
7 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
8 | + import { commonDataSourceSchemas } from '../../../config/common.config'; | |
9 | + | |
10 | + defineProps<{ | |
11 | + values: PublicComponentValueType; | |
12 | + componentConfig: CreateComponentType; | |
13 | + }>(); | |
14 | + | |
15 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | |
16 | + labelWidth: 0, | |
17 | + showActionButtonGroup: false, | |
18 | + layout: 'horizontal', | |
19 | + labelCol: { span: 0 }, | |
20 | + schemas: commonDataSourceSchemas(), | |
21 | + }); | |
22 | + | |
23 | + const getFormValues = () => { | |
24 | + return getFieldsValue(); | |
25 | + }; | |
26 | + | |
27 | + const setFormValues = (record: Recordable) => { | |
28 | + return setFieldsValue(record); | |
29 | + }; | |
30 | + | |
31 | + defineExpose({ | |
32 | + getFormValues, | |
33 | + setFormValues, | |
34 | + validate, | |
35 | + resetFormValues: resetFields, | |
36 | + } as PublicFormInstaceType); | |
37 | +</script> | |
38 | + | |
39 | +<template> | |
40 | + <BasicForm @register="register" /> | |
41 | +</template> | ... | ... |
1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | |
2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | |
3 | + | |
4 | +const componentKeys = useComponentKeys('InstrumentComponent2'); | |
5 | + | |
6 | +export const InstrumentComponent2Config: ConfigType = { | |
7 | + ...componentKeys, | |
8 | + title: '阶段仪表盘', | |
9 | + package: PackagesCategoryEnum.INSTRUMENT, | |
10 | +}; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type'; | |
3 | + import { Gradient, GradientColor, option } from './config'; | |
4 | + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket'; | |
5 | + import { ECharts, EChartsOption, init } from 'echarts'; | |
6 | + import { ref, unref, onMounted, computed } from 'vue'; | |
7 | + import { isArray } from '/@/utils/is'; | |
8 | + import { ComponentInfoGradientInfoType } from '/@/views/visual/palette/types'; | |
9 | + import { useComponentScale } from '/@/views/visual/packages/hook/useComponentScale'; | |
10 | + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime'; | |
11 | + import { useIntervalFn } from '@vueuse/core'; | |
12 | + import { nextTick } from 'vue'; | |
13 | + | |
14 | + const props = defineProps<{ | |
15 | + config: ComponentPropsConfigType<typeof option>; | |
16 | + }>(); | |
17 | + | |
18 | + const time = ref<Nullable<number>>(null); | |
19 | + | |
20 | + useComponentScale(props, async () => { | |
21 | + await nextTick(); | |
22 | + unref(chartInstance)?.resize(); | |
23 | + }); | |
24 | + | |
25 | + const chartRefEl = ref<Nullable<HTMLDivElement>>(null); | |
26 | + | |
27 | + const chartInstance = ref<Nullable<ECharts>>(null); | |
28 | + | |
29 | + const getDesign = computed(() => { | |
30 | + const { option, persetOption } = props.config; | |
31 | + const { componentInfo, attributeRename, attribute } = option; | |
32 | + | |
33 | + const { | |
34 | + fontColor: presetFontColor, | |
35 | + unit: presetUnit, | |
36 | + gradientInfo: presetGradientInfo, | |
37 | + } = persetOption || {}; | |
38 | + | |
39 | + const { unit, fontColor, gradientInfo } = componentInfo || {}; | |
40 | + return { | |
41 | + unit: unit ?? presetUnit, | |
42 | + fontColor: fontColor ?? presetFontColor, | |
43 | + gradientInfo: gradientInfo ?? presetGradientInfo, | |
44 | + attribute: attributeRename || attribute, | |
45 | + }; | |
46 | + }); | |
47 | + | |
48 | + const getGradient = (key: Gradient, record: ComponentInfoGradientInfoType[] = []) => { | |
49 | + if (!isArray(record)) return; | |
50 | + return record.find((item) => item.key === key); | |
51 | + }; | |
52 | + | |
53 | + const options = (): EChartsOption => { | |
54 | + const { gradientInfo, unit, fontColor } = unref(getDesign); | |
55 | + const firstRecord = getGradient(Gradient.FIRST, gradientInfo); | |
56 | + const secondRecord = getGradient(Gradient.SECOND, gradientInfo); | |
57 | + const thirdRecord = getGradient(Gradient.THIRD, gradientInfo); | |
58 | + | |
59 | + let max = thirdRecord?.value || secondRecord?.value || firstRecord?.value || 100; | |
60 | + | |
61 | + max = Number( | |
62 | + 1 + | |
63 | + Array(String(max).length - 1) | |
64 | + .fill(0) | |
65 | + .join('') | |
66 | + ); | |
67 | + | |
68 | + const firstGradient = firstRecord?.value ? firstRecord.value / max : 0.3; | |
69 | + const secondGradient = secondRecord?.value ? secondRecord.value / max : 0.7; | |
70 | + | |
71 | + return { | |
72 | + series: [ | |
73 | + { | |
74 | + type: 'gauge', | |
75 | + min: 0, | |
76 | + max, | |
77 | + axisLine: { | |
78 | + lineStyle: { | |
79 | + width: 20, | |
80 | + color: [ | |
81 | + [firstGradient, firstRecord?.color || GradientColor.FIRST], | |
82 | + [secondGradient, secondRecord?.color || GradientColor.SECOND], | |
83 | + [1, thirdRecord?.color || GradientColor.THIRD], | |
84 | + ], | |
85 | + }, | |
86 | + }, | |
87 | + pointer: { | |
88 | + itemStyle: { | |
89 | + color: 'inherit', | |
90 | + }, | |
91 | + }, | |
92 | + axisTick: { | |
93 | + distance: -30, | |
94 | + length: 8, | |
95 | + splitNumber: max / 100, | |
96 | + lineStyle: { | |
97 | + color: '#fff', | |
98 | + width: 2, | |
99 | + }, | |
100 | + }, | |
101 | + splitLine: { | |
102 | + distance: -10, | |
103 | + length: 30, | |
104 | + lineStyle: { | |
105 | + color: '#fff', | |
106 | + width: 4, | |
107 | + }, | |
108 | + }, | |
109 | + axisLabel: { | |
110 | + color: 'inherit', | |
111 | + distance: 5, | |
112 | + fontSize: 6, | |
113 | + }, | |
114 | + detail: { | |
115 | + valueAnimation: true, | |
116 | + formatter: `{value} ${unit ?? ''}`, | |
117 | + color: fontColor || 'inherit', | |
118 | + offsetCenter: [0, '70%'], | |
119 | + fontSize: 14, | |
120 | + }, | |
121 | + data: [ | |
122 | + { | |
123 | + value: 20, | |
124 | + }, | |
125 | + ], | |
126 | + }, | |
127 | + ], | |
128 | + }; | |
129 | + }; | |
130 | + | |
131 | + const updateChartFn = (value: number) => { | |
132 | + unref(chartInstance)?.setOption({ | |
133 | + series: [{ data: [{ value: value.toFixed(2) }] }], | |
134 | + } as EChartsOption); | |
135 | + }; | |
136 | + | |
137 | + const initial = () => { | |
138 | + chartInstance.value = init(unref(chartRefEl)!); | |
139 | + chartInstance.value.setOption(options()); | |
140 | + }; | |
141 | + | |
142 | + const randomFn = () => { | |
143 | + useIntervalFn(() => { | |
144 | + const value = (Math.random() * 100).toFixed(0); | |
145 | + unref(chartInstance)?.setOption({ | |
146 | + series: [{ data: [{ value }] }], | |
147 | + } as EChartsOption); | |
148 | + }, 3000); | |
149 | + }; | |
150 | + | |
151 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | |
152 | + const { data = {} } = message; | |
153 | + const [latest] = data[attribute] || []; | |
154 | + const [timespan, value] = latest; | |
155 | + time.value = timespan; | |
156 | + updateChartFn(isNaN(value as unknown as number) ? 0 : Number(value)); | |
157 | + }; | |
158 | + | |
159 | + useDataFetch(props, updateFn); | |
160 | + | |
161 | + onMounted(() => { | |
162 | + initial(); | |
163 | + !props.config.option.uuid && randomFn(); | |
164 | + }); | |
165 | +</script> | |
166 | + | |
167 | +<template> | |
168 | + <main class="w-full h-full flex flex-col justify-center items-center"> | |
169 | + <div ref="chartRefEl" class="flex-1 w-full h-full"> </div> | |
170 | + <div class="text-center text-gray-500 text-xs truncate"> | |
171 | + {{ getDesign.attribute || '速度' }} | |
172 | + </div> | |
173 | + <UpdateTime :time="time" /> | |
174 | + </main> | |
175 | +</template> | ... | ... |
1 | +import { DigitalDashboardComponentConfig } from './DigitalDashboardComponent'; | |
2 | +import { InstrumentComponent1Config } from './InstrumentComponent1'; | |
3 | +import { InstrumentComponent2Config } from './InstrumentComponent2'; | |
4 | + | |
5 | +export const InstrumentList = [ | |
6 | + InstrumentComponent1Config, | |
7 | + InstrumentComponent2Config, | |
8 | + DigitalDashboardComponentConfig, | |
9 | +]; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { BasicForm, useForm } from '/@/components/Form'; | |
3 | + import { BasicModal, useModalInner } from '/@/components/Modal'; | |
4 | + import { formSchema, getHistorySearchParams, SchemaFiled } from './history.config'; | |
5 | + import { HistoryModalOkEmitParams } from './type'; | |
6 | + import { ref } from 'vue'; | |
7 | + import { getAllDeviceByOrg } from '/@/api/dataBoard'; | |
8 | + import { getDeviceHistoryInfo } from '/@/api/alarm/position'; | |
9 | + import { DataSource } from '/@/views/visual/palette/types'; | |
10 | + import { cloneDeep } from 'lodash-es'; | |
11 | + | |
12 | + const emit = defineEmits(['register', 'ok']); | |
13 | + | |
14 | + const [registerForm, { updateSchema, setFieldsValue, validate, getFieldsValue }] = useForm({ | |
15 | + schemas: formSchema(), | |
16 | + showActionButtonGroup: false, | |
17 | + fieldMapToTime: [ | |
18 | + [SchemaFiled.DATE_RANGE, [SchemaFiled.START_TS, SchemaFiled.END_TS], 'YYYY-MM-DD HH:mm:ss'], | |
19 | + ], | |
20 | + }); | |
21 | + | |
22 | + const [registerModal, { closeModal }] = useModalInner(async (dataSource: DataSource[]) => { | |
23 | + try { | |
24 | + dataSource = cloneDeep(dataSource); | |
25 | + if (dataSource.length < 2) return; | |
26 | + dataSource = dataSource.splice(0, 2); | |
27 | + const deviceRecord = dataSource?.at(0) || ({} as DataSource); | |
28 | + if (!deviceRecord.organizationId) return; | |
29 | + const deviceList = await getAllDeviceByOrg( | |
30 | + deviceRecord.organizationId, | |
31 | + deviceRecord.deviceProfileId | |
32 | + ); | |
33 | + const options = deviceList | |
34 | + .filter((item) => item.tbDeviceId === deviceRecord.deviceId) | |
35 | + .map((item) => ({ ...item, label: item.name, value: item.tbDeviceId })); | |
36 | + | |
37 | + const attKey = dataSource.map((item) => ({ | |
38 | + ...item, | |
39 | + label: item.attribute, | |
40 | + value: item.attribute, | |
41 | + })); | |
42 | + updateSchema([ | |
43 | + { | |
44 | + field: SchemaFiled.DEVICE_ID, | |
45 | + componentProps: { | |
46 | + options, | |
47 | + }, | |
48 | + }, | |
49 | + { | |
50 | + field: SchemaFiled.KEYS, | |
51 | + component: 'Select', | |
52 | + defaultValue: attKey.map((item) => item.value), | |
53 | + componentProps: { | |
54 | + options: attKey, | |
55 | + mode: 'multiple', | |
56 | + disabled: true, | |
57 | + }, | |
58 | + }, | |
59 | + ]); | |
60 | + | |
61 | + setFieldsValue({ | |
62 | + [SchemaFiled.DEVICE_ID]: deviceRecord.deviceId, | |
63 | + [SchemaFiled.KEYS]: attKey.map((item) => item.value), | |
64 | + }); | |
65 | + } catch (error) { | |
66 | + throw error; | |
67 | + } | |
68 | + }); | |
69 | + | |
70 | + const validEffective = (value = '') => { | |
71 | + return !!(value && !isNaN(value as unknown as number)); | |
72 | + }; | |
73 | + const loading = ref(false); | |
74 | + const handleOk = async () => { | |
75 | + try { | |
76 | + await validate(); | |
77 | + let value = getFieldsValue(); | |
78 | + | |
79 | + value = getHistorySearchParams(value); | |
80 | + | |
81 | + loading.value = true; | |
82 | + | |
83 | + const res = await getDeviceHistoryInfo({ | |
84 | + ...value, | |
85 | + [SchemaFiled.KEYS]: value[SchemaFiled.KEYS].join(','), | |
86 | + }); | |
87 | + | |
88 | + let timespanList = Object.keys(res).reduce((prev, next) => { | |
89 | + const ts = res[next].map((item) => item.ts); | |
90 | + return [...prev, ...ts]; | |
91 | + }, [] as number[]); | |
92 | + timespanList = [...new Set(timespanList)]; | |
93 | + | |
94 | + const track: Record<'lng' | 'lat', number>[] = []; | |
95 | + const keys = Object.keys(res); | |
96 | + | |
97 | + for (const ts of timespanList) { | |
98 | + const list: { ts: number; value: number }[] = []; | |
99 | + for (const key of keys) { | |
100 | + const record = res[key].find((item) => ts === item.ts); | |
101 | + if (!validEffective(record?.value)) { | |
102 | + continue; | |
103 | + } | |
104 | + list.push(record as any); | |
105 | + } | |
106 | + | |
107 | + if (list.length === 2 && list.every(Boolean)) { | |
108 | + const lng = list.at(0)?.value; | |
109 | + const lat = list.at(1)?.value; | |
110 | + if (lng && lat) track.push({ lng, lat }); | |
111 | + } | |
112 | + } | |
113 | + | |
114 | + emit('ok', { track, value } as HistoryModalOkEmitParams); | |
115 | + closeModal(); | |
116 | + } catch (error) { | |
117 | + throw error; | |
118 | + } finally { | |
119 | + loading.value = false; | |
120 | + } | |
121 | + }; | |
122 | +</script> | |
123 | + | |
124 | +<template> | |
125 | + <BasicModal | |
126 | + title="历史轨迹" | |
127 | + @register="registerModal" | |
128 | + @ok="handleOk" | |
129 | + :ok-button-props="{ loading }" | |
130 | + > | |
131 | + <BasicForm @register="registerForm" /> | |
132 | + </BasicModal> | |
133 | +</template> | ... | ... |
1 | +import cloneDeep from 'lodash-es/cloneDeep'; | |
2 | +import { MapComponentTrackHistoryConfig } from '.'; | |
3 | +import { | |
4 | + ConfigType, | |
5 | + CreateComponentType, | |
6 | + PublicComponentOptions, | |
7 | + PublicPresetOptions, | |
8 | +} from '/@/views/visual/packages/index.type'; | |
9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | |
10 | + | |
11 | +export const option: PublicPresetOptions = { | |
12 | + componetDesign: false, | |
13 | + multipleDataSourceComponent: true, | |
14 | +}; | |
15 | + | |
16 | +export default class Config extends PublicConfigClass implements CreateComponentType { | |
17 | + public key: string = MapComponentTrackHistoryConfig.key; | |
18 | + | |
19 | + public attr = { ...componentInitConfig }; | |
20 | + | |
21 | + public componentConfig: ConfigType = cloneDeep(MapComponentTrackHistoryConfig); | |
22 | + | |
23 | + public persetOption = cloneDeep(option); | |
24 | + | |
25 | + public option: PublicComponentOptions; | |
26 | + | |
27 | + constructor(option: PublicComponentOptions) { | |
28 | + super(); | |
29 | + this.option = { ...option }; | |
30 | + } | |
31 | +} | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | |
3 | + import { useForm, BasicForm } from '/@/components/Form'; | |
4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
5 | + | |
6 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | |
7 | + schemas: [ | |
8 | + { | |
9 | + field: ComponentConfigFieldEnum.FONT_COLOR, | |
10 | + label: '数值字体颜色', | |
11 | + component: 'ColorPicker', | |
12 | + changeEvent: 'update:value', | |
13 | + componentProps: { | |
14 | + defaultValue: '#FD7347', | |
15 | + }, | |
16 | + }, | |
17 | + { | |
18 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
19 | + label: '显示设备名称', | |
20 | + component: 'Checkbox', | |
21 | + }, | |
22 | + ], | |
23 | + showActionButtonGroup: false, | |
24 | + labelWidth: 120, | |
25 | + baseColProps: { | |
26 | + span: 12, | |
27 | + }, | |
28 | + }); | |
29 | + | |
30 | + const getFormValues = () => { | |
31 | + return getFieldsValue(); | |
32 | + }; | |
33 | + | |
34 | + const setFormValues = (data: Recordable) => { | |
35 | + return setFieldsValue(data); | |
36 | + }; | |
37 | + | |
38 | + defineExpose({ | |
39 | + getFormValues, | |
40 | + setFormValues, | |
41 | + resetFormValues: resetFields, | |
42 | + } as PublicFormInstaceType); | |
43 | +</script> | |
44 | + | |
45 | +<template> | |
46 | + <BasicForm @register="register" /> | |
47 | +</template> | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { CreateComponentType } from '/@/views/visual/packages/index.type'; | |
3 | + import { BasicForm, useForm } from '/@/components/Form'; | |
4 | + import { | |
5 | + PublicComponentValueType, | |
6 | + PublicFormInstaceType, | |
7 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | |
8 | + import { commonDataSourceSchemas } from '../../../config/common.config'; | |
9 | + | |
10 | + defineProps<{ | |
11 | + values: PublicComponentValueType; | |
12 | + componentConfig: CreateComponentType; | |
13 | + }>(); | |
14 | + | |
15 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | |
16 | + labelWidth: 0, | |
17 | + showActionButtonGroup: false, | |
18 | + layout: 'horizontal', | |
19 | + labelCol: { span: 0 }, | |
20 | + schemas: commonDataSourceSchemas(), | |
21 | + }); | |
22 | + | |
23 | + const getFormValues = () => { | |
24 | + return getFieldsValue(); | |
25 | + }; | |
26 | + | |
27 | + const setFormValues = (record: Recordable) => { | |
28 | + return setFieldsValue(record); | |
29 | + }; | |
30 | + | |
31 | + defineExpose({ | |
32 | + getFormValues, | |
33 | + setFormValues, | |
34 | + validate, | |
35 | + resetFormValues: resetFields, | |
36 | + } as PublicFormInstaceType); | |
37 | +</script> | |
38 | + | |
39 | +<template> | |
40 | + <BasicForm @register="register" /> | |
41 | +</template> | ... | ... |
1 | +import moment from 'moment'; | |
2 | +import { Moment } from 'moment'; | |
3 | +import { FormSchema } from '/@/components/Form'; | |
4 | +import { ColEx } from '/@/components/Form/src/types'; | |
5 | +import { useGridLayout } from '/@/hooks/component/useGridLayout'; | |
6 | +import { | |
7 | + getPacketIntervalByRange, | |
8 | + getPacketIntervalByValue, | |
9 | + intervalOption, | |
10 | +} from '/@/views/device/localtion/cpns/TimePeriodForm/helper'; | |
11 | +export enum QueryWay { | |
12 | + LATEST = 'latest', | |
13 | + TIME_PERIOD = 'timePeriod', | |
14 | +} | |
15 | + | |
16 | +export enum SchemaFiled { | |
17 | + DEVICE_ID = 'deviceId', | |
18 | + WAY = 'way', | |
19 | + TIME_PERIOD = 'timePeriod', | |
20 | + KEYS = 'keys', | |
21 | + DATE_RANGE = 'dataRange', | |
22 | + START_TS = 'startTs', | |
23 | + END_TS = 'endTs', | |
24 | + INTERVAL = 'interval', | |
25 | + LIMIT = 'limit', | |
26 | + AGG = 'agg', | |
27 | + ORDER_BY = 'orderBy', | |
28 | +} | |
29 | + | |
30 | +export enum AggregateDataEnum { | |
31 | + MIN = 'MIN', | |
32 | + MAX = 'MAX', | |
33 | + AVG = 'AVG', | |
34 | + SUM = 'SUM', | |
35 | + COUNT = 'COUNT', | |
36 | + NONE = 'NONE', | |
37 | +} | |
38 | +export const formSchema = (): FormSchema[] => { | |
39 | + return [ | |
40 | + { | |
41 | + field: SchemaFiled.DEVICE_ID, | |
42 | + label: '设备名称', | |
43 | + component: 'Select', | |
44 | + rules: [{ required: true, message: '设备名称为必选项', type: 'string' }], | |
45 | + componentProps({ formActionType }) { | |
46 | + const { setFieldsValue } = formActionType; | |
47 | + return { | |
48 | + placeholder: '请选择设备', | |
49 | + onChange() { | |
50 | + setFieldsValue({ [SchemaFiled.KEYS]: null }); | |
51 | + }, | |
52 | + }; | |
53 | + }, | |
54 | + }, | |
55 | + { | |
56 | + field: SchemaFiled.WAY, | |
57 | + label: '查询方式', | |
58 | + component: 'RadioGroup', | |
59 | + defaultValue: QueryWay.LATEST, | |
60 | + componentProps({ formActionType }) { | |
61 | + const { setFieldsValue } = formActionType; | |
62 | + return { | |
63 | + options: [ | |
64 | + { label: '最后', value: QueryWay.LATEST }, | |
65 | + { label: '时间段', value: QueryWay.TIME_PERIOD }, | |
66 | + ], | |
67 | + onChange(event: ChangeEvent) { | |
68 | + (event.target as HTMLInputElement).value === QueryWay.LATEST | |
69 | + ? setFieldsValue({ | |
70 | + [SchemaFiled.DATE_RANGE]: [], | |
71 | + [SchemaFiled.START_TS]: null, | |
72 | + [SchemaFiled.END_TS]: null, | |
73 | + }) | |
74 | + : setFieldsValue({ [SchemaFiled.START_TS]: null }); | |
75 | + }, | |
76 | + getPopupContainer: () => document.body, | |
77 | + }; | |
78 | + }, | |
79 | + }, | |
80 | + { | |
81 | + field: SchemaFiled.START_TS, | |
82 | + label: '最后数据', | |
83 | + component: 'Select', | |
84 | + ifShow({ values }) { | |
85 | + return values[SchemaFiled.WAY] === QueryWay.LATEST; | |
86 | + }, | |
87 | + componentProps({ formActionType }) { | |
88 | + const { setFieldsValue } = formActionType; | |
89 | + return { | |
90 | + options: intervalOption, | |
91 | + onChange() { | |
92 | + setFieldsValue({ [SchemaFiled.INTERVAL]: null }); | |
93 | + }, | |
94 | + getPopupContainer: () => document.body, | |
95 | + }; | |
96 | + }, | |
97 | + rules: [{ required: true, message: '最后数据为必选项', type: 'number' }], | |
98 | + }, | |
99 | + { | |
100 | + field: SchemaFiled.DATE_RANGE, | |
101 | + label: '时间段', | |
102 | + component: 'RangePicker', | |
103 | + ifShow({ values }) { | |
104 | + return values[SchemaFiled.WAY] === QueryWay.TIME_PERIOD; | |
105 | + }, | |
106 | + rules: [{ required: true, message: '时间段为必选项' }], | |
107 | + componentProps({ formActionType }) { | |
108 | + const { setFieldsValue } = formActionType; | |
109 | + let dates: Moment[] = []; | |
110 | + return { | |
111 | + showTime: { | |
112 | + defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')], | |
113 | + }, | |
114 | + onCalendarChange(value: Moment[]) { | |
115 | + dates = value; | |
116 | + }, | |
117 | + disabledDate(current: Moment) { | |
118 | + if (!dates || dates.length === 0 || !current) { | |
119 | + return false; | |
120 | + } | |
121 | + const diffDate = current.diff(dates[0], 'years', true); | |
122 | + return Math.abs(diffDate) > 1; | |
123 | + }, | |
124 | + onChange() { | |
125 | + dates = []; | |
126 | + setFieldsValue({ [SchemaFiled.INTERVAL]: null }); | |
127 | + }, | |
128 | + getPopupContainer: () => document.body, | |
129 | + }; | |
130 | + }, | |
131 | + colProps: useGridLayout(2, 2, 2, 2, 2, 2) as unknown as ColEx, | |
132 | + }, | |
133 | + { | |
134 | + field: SchemaFiled.AGG, | |
135 | + label: '数据聚合功能', | |
136 | + component: 'Select', | |
137 | + componentProps: { | |
138 | + getPopupContainer: () => document.body, | |
139 | + options: [ | |
140 | + { label: '最小值', value: AggregateDataEnum.MIN }, | |
141 | + { label: '最大值', value: AggregateDataEnum.MAX }, | |
142 | + { label: '平均值', value: AggregateDataEnum.AVG }, | |
143 | + { label: '求和', value: AggregateDataEnum.SUM }, | |
144 | + { label: '计数', value: AggregateDataEnum.COUNT }, | |
145 | + { label: '空', value: AggregateDataEnum.NONE }, | |
146 | + ], | |
147 | + }, | |
148 | + }, | |
149 | + { | |
150 | + field: SchemaFiled.INTERVAL, | |
151 | + label: '分组间隔', | |
152 | + component: 'Select', | |
153 | + dynamicRules: ({ model }) => { | |
154 | + return [ | |
155 | + { | |
156 | + required: model[SchemaFiled.AGG] !== AggregateDataEnum.NONE, | |
157 | + message: '分组间隔为必填项', | |
158 | + type: 'number', | |
159 | + }, | |
160 | + ]; | |
161 | + }, | |
162 | + ifShow({ values }) { | |
163 | + return values[SchemaFiled.AGG] !== AggregateDataEnum.NONE; | |
164 | + }, | |
165 | + componentProps({ formModel, formActionType }) { | |
166 | + const options = | |
167 | + formModel[SchemaFiled.WAY] === QueryWay.LATEST | |
168 | + ? getPacketIntervalByValue(formModel[SchemaFiled.START_TS]) | |
169 | + : getPacketIntervalByRange(formModel[SchemaFiled.DATE_RANGE]); | |
170 | + if (formModel[SchemaFiled.AGG] !== AggregateDataEnum.NONE) { | |
171 | + formActionType.setFieldsValue({ [SchemaFiled.LIMIT]: null }); | |
172 | + } | |
173 | + return { | |
174 | + options, | |
175 | + getPopupContainer: () => document.body, | |
176 | + }; | |
177 | + }, | |
178 | + }, | |
179 | + { | |
180 | + field: SchemaFiled.LIMIT, | |
181 | + label: '最大条数', | |
182 | + component: 'InputNumber', | |
183 | + ifShow({ values }) { | |
184 | + return values[SchemaFiled.AGG] === AggregateDataEnum.NONE; | |
185 | + }, | |
186 | + rules: [{ required: true, message: '最大条数为必填项' }], | |
187 | + helpMessage: ['根据查询条件,查出的数据条数不超过这个值'], | |
188 | + componentProps() { | |
189 | + return { | |
190 | + max: 50000, | |
191 | + min: 7, | |
192 | + getPopupContainer: () => document.body, | |
193 | + }; | |
194 | + }, | |
195 | + }, | |
196 | + { | |
197 | + field: SchemaFiled.KEYS, | |
198 | + label: '设备属性', | |
199 | + component: 'Select', | |
200 | + componentProps: { | |
201 | + getPopupContainer: () => document.body, | |
202 | + }, | |
203 | + }, | |
204 | + ]; | |
205 | +}; | |
206 | + | |
207 | +export function getHistorySearchParams(value: Recordable) { | |
208 | + const { startTs, endTs, interval, agg, limit, way, keys, deviceId } = value; | |
209 | + if (way === QueryWay.LATEST) { | |
210 | + return { | |
211 | + keys, | |
212 | + entityId: deviceId, | |
213 | + startTs: moment().subtract(startTs, 'ms').valueOf(), | |
214 | + endTs: Date.now(), | |
215 | + interval, | |
216 | + agg, | |
217 | + limit, | |
218 | + }; | |
219 | + } else { | |
220 | + return { | |
221 | + keys, | |
222 | + entityId: deviceId, | |
223 | + startTs: moment(startTs).valueOf(), | |
224 | + endTs: moment(endTs).valueOf(), | |
225 | + interval, | |
226 | + agg, | |
227 | + limit, | |
228 | + }; | |
229 | + } | |
230 | +} | ... | ... |
1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | |
2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | |
3 | + | |
4 | +const componentKeys = useComponentKeys('MapComponentTrackHistory'); | |
5 | + | |
6 | +export const MapComponentTrackHistoryConfig: ConfigType = { | |
7 | + ...componentKeys, | |
8 | + title: '历史轨迹', | |
9 | + package: PackagesCategoryEnum.MAP, | |
10 | +}; | ... | ... |