Showing
150 changed files
with
8851 additions
and
606 deletions
| @@ -5,7 +5,9 @@ | @@ -5,7 +5,9 @@ | ||
| 5 | "public/resource/tinymce/langs" | 5 | "public/resource/tinymce/langs" |
| 6 | ], | 6 | ], |
| 7 | "cSpell.words": [ | 7 | "cSpell.words": [ |
| 8 | + "Cmds", | ||
| 8 | "COAP", | 9 | "COAP", |
| 10 | + "echarts", | ||
| 9 | "edrx", | 11 | "edrx", |
| 10 | "EFENTO", | 12 | "EFENTO", |
| 11 | "inited", | 13 | "inited", |
| @@ -17,6 +19,8 @@ | @@ -17,6 +19,8 @@ | ||
| 17 | "unref", | 19 | "unref", |
| 18 | "vben", | 20 | "vben", |
| 19 | "VITE", | 21 | "VITE", |
| 22 | + "vnode", | ||
| 23 | + "vueuse", | ||
| 20 | "windicss" | 24 | "windicss" |
| 21 | ] | 25 | ] |
| 22 | } | 26 | } |
| 1 | <script lang="ts" setup> | 1 | <script lang="ts" setup> |
| 2 | import '@simonwep/pickr/dist/themes/monolith.min.css'; | 2 | import '@simonwep/pickr/dist/themes/monolith.min.css'; |
| 3 | import ColorPicker from '@simonwep/pickr'; | 3 | import ColorPicker from '@simonwep/pickr'; |
| 4 | - import type { PropType } from 'vue'; | 4 | + import { PropType, watch } from 'vue'; |
| 5 | import { computed, onMounted, onUnmounted, ref, unref } from 'vue'; | 5 | import { computed, onMounted, onUnmounted, ref, unref } from 'vue'; |
| 6 | 6 | ||
| 7 | type Format = Exclude<keyof ColorPicker.HSVaColor, 'clone'>; | 7 | type Format = Exclude<keyof ColorPicker.HSVaColor, 'clone'>; |
| @@ -18,10 +18,6 @@ | @@ -18,10 +18,6 @@ | ||
| 18 | config: { | 18 | config: { |
| 19 | type: Object as PropType<ColorPicker.Options>, | 19 | type: Object as PropType<ColorPicker.Options>, |
| 20 | }, | 20 | }, |
| 21 | - defaultValue: { | ||
| 22 | - type: String, | ||
| 23 | - default: '', | ||
| 24 | - }, | ||
| 25 | }); | 21 | }); |
| 26 | 22 | ||
| 27 | const emit = defineEmits(['update:value']); | 23 | const emit = defineEmits(['update:value']); |
| @@ -52,11 +48,16 @@ | @@ -52,11 +48,16 @@ | ||
| 52 | }; | 48 | }; |
| 53 | 49 | ||
| 54 | const onClear = () => { | 50 | const onClear = () => { |
| 55 | - emit('update:value', props.defaultValue); | 51 | + emit('update:value', props.value); |
| 56 | unref(picker)?.hide(); | 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 | const getOption = computed<ColorPicker.Options>(() => { | 61 | const getOption = computed<ColorPicker.Options>(() => { |
| 61 | const { config = {} } = props; | 62 | const { config = {} } = props; |
| 62 | return { | 63 | return { |
| @@ -100,17 +101,28 @@ | @@ -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 | onMounted(() => { | 113 | onMounted(() => { |
| 104 | picker.value = ColorPicker.create(unref(getOption)); | 114 | picker.value = ColorPicker.create(unref(getOption)); |
| 105 | unref(picker)?.on('init', onInit); | 115 | unref(picker)?.on('init', onInit); |
| 106 | unref(picker)?.on('save', onSave); | 116 | unref(picker)?.on('save', onSave); |
| 107 | unref(picker)?.on('clear', onClear); | 117 | unref(picker)?.on('clear', onClear); |
| 118 | + // unref(picker)?.on('change', onChange); | ||
| 108 | }); | 119 | }); |
| 109 | 120 | ||
| 110 | onUnmounted(() => { | 121 | onUnmounted(() => { |
| 111 | unref(picker)?.off('init', onInit); | 122 | unref(picker)?.off('init', onInit); |
| 112 | unref(picker)?.off('save', onSave); | 123 | unref(picker)?.off('save', onSave); |
| 113 | unref(picker)?.off('clear', onClear); | 124 | unref(picker)?.off('clear', onClear); |
| 125 | + // unref(picker)?.off('change', onChange); | ||
| 114 | 126 | ||
| 115 | unref(picker)?.destroyAndRemove(); | 127 | unref(picker)?.destroyAndRemove(); |
| 116 | }); | 128 | }); |
| @@ -3,6 +3,7 @@ export enum DataActionModeEnum { | @@ -3,6 +3,7 @@ export enum DataActionModeEnum { | ||
| 3 | READ = 'READ', | 3 | READ = 'READ', |
| 4 | UPDATE = 'UPDATE', | 4 | UPDATE = 'UPDATE', |
| 5 | DELETE = 'DELETE', | 5 | DELETE = 'DELETE', |
| 6 | + COPY = 'COPY', | ||
| 6 | } | 7 | } |
| 7 | 8 | ||
| 8 | export enum DataActionModeNameEnum { | 9 | export enum DataActionModeNameEnum { |
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,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 | return [ | 109 | return [ |
| 107 | { | 110 | { |
| 108 | field: DataSourceField.IS_GATEWAY_DEVICE, | 111 | field: DataSourceField.IS_GATEWAY_DEVICE, |
| 1 | <script lang="ts" setup> | 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 | </script> | 3 | </script> |
| 388 | - | ||
| 389 | <template> | 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 | </template> | 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 { RectFlowmeteConfig } from '.'; | ||
| 3 | +import { | ||
| 4 | + ConfigType, | ||
| 5 | + CreateComponentType, | ||
| 6 | + PublicComponentOptions, | ||
| 7 | + PublicPresetOptions, | ||
| 8 | +} from '/@/views/visual/packages/index.type'; | ||
| 9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | ||
| 10 | +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 11 | + | ||
| 12 | +export const option: PublicPresetOptions = { | ||
| 13 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false, | ||
| 14 | + [ComponentConfigFieldEnum.UNIT]: 'm', | ||
| 15 | + [ComponentConfigFieldEnum.FLOWMETER_CONFIG]: { | ||
| 16 | + [ComponentConfigFieldEnum.BACKGROUND_COLOR]: '#8badcb', | ||
| 17 | + [ComponentConfigFieldEnum.WAVE_FIRST]: '#4579e2', | ||
| 18 | + [ComponentConfigFieldEnum.WAVE_SECOND]: '#3461c1', | ||
| 19 | + [ComponentConfigFieldEnum.WAVE_THIRD]: '#2d55aa', | ||
| 20 | + }, | ||
| 21 | +}; | ||
| 22 | + | ||
| 23 | +export default class Config extends PublicConfigClass implements CreateComponentType { | ||
| 24 | + public key: string = RectFlowmeteConfig.key; | ||
| 25 | + | ||
| 26 | + public attr = { ...componentInitConfig }; | ||
| 27 | + | ||
| 28 | + public componentConfig: ConfigType = cloneDeep(RectFlowmeteConfig); | ||
| 29 | + | ||
| 30 | + public persetOption = cloneDeep(option); | ||
| 31 | + | ||
| 32 | + public option: PublicComponentOptions; | ||
| 33 | + | ||
| 34 | + constructor(option: PublicComponentOptions) { | ||
| 35 | + super(); | ||
| 36 | + this.option = { ...option }; | ||
| 37 | + } | ||
| 38 | +} |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 3 | + import { useForm, BasicForm } from '/@/components/Form'; | ||
| 4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 5 | + import { option } from './config'; | ||
| 6 | + | ||
| 7 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | ||
| 8 | + schemas: [ | ||
| 9 | + { | ||
| 10 | + field: ComponentConfigFieldEnum.BACKGROUND_COLOR, | ||
| 11 | + label: '背景颜色', | ||
| 12 | + component: 'ColorPicker', | ||
| 13 | + changeEvent: 'update:value', | ||
| 14 | + defaultValue: option.flowmeterConfig?.backgroundColor, | ||
| 15 | + }, | ||
| 16 | + { | ||
| 17 | + field: ComponentConfigFieldEnum.WAVE_FIRST, | ||
| 18 | + label: '浪1', | ||
| 19 | + component: 'ColorPicker', | ||
| 20 | + changeEvent: 'update:value', | ||
| 21 | + defaultValue: option.flowmeterConfig?.waveFirst, | ||
| 22 | + }, | ||
| 23 | + { | ||
| 24 | + field: ComponentConfigFieldEnum.WAVE_SECOND, | ||
| 25 | + label: '浪2', | ||
| 26 | + component: 'ColorPicker', | ||
| 27 | + changeEvent: 'update:value', | ||
| 28 | + defaultValue: option.flowmeterConfig?.waveSecond, | ||
| 29 | + }, | ||
| 30 | + { | ||
| 31 | + field: ComponentConfigFieldEnum.WAVE_THIRD, | ||
| 32 | + label: '浪3', | ||
| 33 | + component: 'ColorPicker', | ||
| 34 | + changeEvent: 'update:value', | ||
| 35 | + defaultValue: option.flowmeterConfig?.waveThird, | ||
| 36 | + }, | ||
| 37 | + { | ||
| 38 | + field: ComponentConfigFieldEnum.UNIT, | ||
| 39 | + label: '单位', | ||
| 40 | + component: 'Input', | ||
| 41 | + defaultValue: option.unit, | ||
| 42 | + }, | ||
| 43 | + ], | ||
| 44 | + showActionButtonGroup: false, | ||
| 45 | + labelWidth: 120, | ||
| 46 | + baseColProps: { | ||
| 47 | + span: 12, | ||
| 48 | + }, | ||
| 49 | + }); | ||
| 50 | + | ||
| 51 | + const getFormValues = () => { | ||
| 52 | + return getFieldsValue(); | ||
| 53 | + }; | ||
| 54 | + | ||
| 55 | + const setFormValues = (data: Recordable) => { | ||
| 56 | + return setFieldsValue(data); | ||
| 57 | + }; | ||
| 58 | + | ||
| 59 | + defineExpose({ | ||
| 60 | + getFormValues, | ||
| 61 | + setFormValues, | ||
| 62 | + resetFormValues: resetFields, | ||
| 63 | + } as PublicFormInstaceType); | ||
| 64 | +</script> | ||
| 65 | + | ||
| 66 | +<template> | ||
| 67 | + <BasicForm @register="register" /> | ||
| 68 | +</template> |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { CreateComponentType } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
| 4 | + import { dataSourceSchema } from '/@/views/visual/board/detail/config/basicConfiguration'; | ||
| 5 | + import { | ||
| 6 | + PublicComponentValueType, | ||
| 7 | + PublicFormInstaceType, | ||
| 8 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 9 | + | ||
| 10 | + const props = defineProps<{ | ||
| 11 | + values: PublicComponentValueType; | ||
| 12 | + componentConfig: CreateComponentType; | ||
| 13 | + }>(); | ||
| 14 | + | ||
| 15 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | ||
| 16 | + labelWidth: 0, | ||
| 17 | + showActionButtonGroup: false, | ||
| 18 | + layout: 'horizontal', | ||
| 19 | + labelCol: { span: 0 }, | ||
| 20 | + schemas: dataSourceSchema(false, props.componentConfig.componentConfig.key), | ||
| 21 | + }); | ||
| 22 | + | ||
| 23 | + const getFormValues = () => { | ||
| 24 | + return getFieldsValue(); | ||
| 25 | + }; | ||
| 26 | + | ||
| 27 | + const setFormValues = (record: Recordable) => { | ||
| 28 | + return setFieldsValue(record); | ||
| 29 | + }; | ||
| 30 | + | ||
| 31 | + defineExpose({ | ||
| 32 | + getFormValues, | ||
| 33 | + setFormValues, | ||
| 34 | + validate, | ||
| 35 | + resetFormValues: resetFields, | ||
| 36 | + } as PublicFormInstaceType); | ||
| 37 | +</script> | ||
| 38 | + | ||
| 39 | +<template> | ||
| 40 | + <BasicForm @register="register" /> | ||
| 41 | +</template> |
| 1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | ||
| 2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | ||
| 3 | + | ||
| 4 | +const componentKeys = useComponentKeys('RectFlowmeter'); | ||
| 5 | + | ||
| 6 | +export const RectFlowmeteConfig: ConfigType = { | ||
| 7 | + ...componentKeys, | ||
| 8 | + title: '流量计1', | ||
| 9 | + package: PackagesCategoryEnum.FLOWMETER, | ||
| 10 | +}; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { option } from './config'; | ||
| 4 | + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket'; | ||
| 5 | + import { computed } from 'vue'; | ||
| 6 | + import { ref } from 'vue'; | ||
| 7 | + import { unref } from 'vue'; | ||
| 8 | + | ||
| 9 | + const props = defineProps<{ | ||
| 10 | + config: ComponentPropsConfigType<typeof option>; | ||
| 11 | + }>(); | ||
| 12 | + | ||
| 13 | + const currentValue = ref(0); | ||
| 14 | + | ||
| 15 | + const getDesign = computed(() => { | ||
| 16 | + const { option, persetOption } = props.config; | ||
| 17 | + const { componentInfo } = option; | ||
| 18 | + const { flowmeterConfig, unit } = componentInfo; | ||
| 19 | + const { backgroundColor, waveFirst, waveSecond, waveThird } = flowmeterConfig || {}; | ||
| 20 | + const { flowmeterConfig: presetFlowmeterConfig, unit: persetUnit } = persetOption || {}; | ||
| 21 | + const { | ||
| 22 | + backgroundColor: presetBackgroundColor, | ||
| 23 | + waveFirst: presetWaveFirst, | ||
| 24 | + waveSecond: presetWaveSecond, | ||
| 25 | + waveThird: presetWaveThird, | ||
| 26 | + } = presetFlowmeterConfig || {}; | ||
| 27 | + return { | ||
| 28 | + backgroundColor: backgroundColor ?? presetBackgroundColor, | ||
| 29 | + waveFirst: waveFirst ?? presetWaveFirst, | ||
| 30 | + waveSecond: waveSecond ?? presetWaveSecond, | ||
| 31 | + waveThird: waveThird ?? presetWaveThird, | ||
| 32 | + unit: unit ?? persetUnit, | ||
| 33 | + }; | ||
| 34 | + }); | ||
| 35 | + | ||
| 36 | + const getWaveHeight = computed(() => { | ||
| 37 | + const value = unref(currentValue); | ||
| 38 | + return value <= 0 ? 0 : -value - 15; | ||
| 39 | + }); | ||
| 40 | + | ||
| 41 | + const getHeight = computed(() => { | ||
| 42 | + const value = unref(currentValue); | ||
| 43 | + return value >= 100 ? 0 : 100 - value + 10; | ||
| 44 | + }); | ||
| 45 | + | ||
| 46 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | ||
| 47 | + const { data = {} } = message; | ||
| 48 | + const [latest] = data[attribute] || []; | ||
| 49 | + const [_, value] = latest; | ||
| 50 | + currentValue.value = Number(value); | ||
| 51 | + }; | ||
| 52 | + | ||
| 53 | + useDataFetch(props, updateFn); | ||
| 54 | +</script> | ||
| 55 | + | ||
| 56 | +<template> | ||
| 57 | + <main class="w-full h-full flex flex-col justify-center items-center"> | ||
| 58 | + <svg | ||
| 59 | + class="waves-rect" | ||
| 60 | + viewBox="0 0 100 100" | ||
| 61 | + preserveAspectRatio="none" | ||
| 62 | + xmlns="http://www.w3.org/2000/svg" | ||
| 63 | + xmlns:xlink="http://www.w3.org/1999/xlink" | ||
| 64 | + > | ||
| 65 | + <defs> | ||
| 66 | + <path | ||
| 67 | + id="wave" | ||
| 68 | + d="M-160 118c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v100h-352z" | ||
| 69 | + /> | ||
| 70 | + </defs> | ||
| 71 | + <rect | ||
| 72 | + class="bgColor" | ||
| 73 | + x="0" | ||
| 74 | + y="0" | ||
| 75 | + width="100" | ||
| 76 | + height="100" | ||
| 77 | + :fill="getDesign.backgroundColor" | ||
| 78 | + /> | ||
| 79 | + <g class="height" :transform="`translate(0 ${getWaveHeight})`"> | ||
| 80 | + <use class="wave waveFirst" xlink:href="#wave" :fill="getDesign.waveFirst" x="0" y="0" /> | ||
| 81 | + <use class="wave waveSecond" xlink:href="#wave" :fill="getDesign.waveSecond" x="0" y="2" /> | ||
| 82 | + <use class="wave waveThird" xlink:href="#wave" :fill="getDesign.waveThird" x="0" y="4" /> | ||
| 83 | + </g> | ||
| 84 | + <rect | ||
| 85 | + class="waveThird" | ||
| 86 | + :transform="`translate(0 ${getHeight})`" | ||
| 87 | + x="0" | ||
| 88 | + y="0" | ||
| 89 | + width="100" | ||
| 90 | + height="100" | ||
| 91 | + :fill="getDesign.waveThird" | ||
| 92 | + /> | ||
| 93 | + | ||
| 94 | + <foreignObject | ||
| 95 | + x="0" | ||
| 96 | + y="0" | ||
| 97 | + width="100" | ||
| 98 | + height="100" | ||
| 99 | + text-anchor="middle" | ||
| 100 | + dominant-baseline="middle" | ||
| 101 | + > | ||
| 102 | + <div xmlns="http://www.w3.org/1999/xhtml" class="text"> | ||
| 103 | + <span>{{ currentValue }}</span> | ||
| 104 | + <span class="ml-1">{{ getDesign.unit }}</span> | ||
| 105 | + </div> | ||
| 106 | + </foreignObject> | ||
| 107 | + </svg> | ||
| 108 | + </main> | ||
| 109 | +</template> | ||
| 110 | + | ||
| 111 | +<style lang="less" scoped> | ||
| 112 | + .waves-rect { | ||
| 113 | + width: 90%; | ||
| 114 | + height: 90%; | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + @keyframes move { | ||
| 118 | + from { | ||
| 119 | + transform: translate(-90px, 0%); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + to { | ||
| 123 | + transform: translate(85px, 0%); | ||
| 124 | + } | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + .wave { | ||
| 128 | + animation: move 3s linear infinite; | ||
| 129 | + animation-play-state: running; | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + .wave:nth-child(1) { | ||
| 133 | + animation-delay: -2s; | ||
| 134 | + animation-duration: 9s; | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + .wave:nth-child(2) { | ||
| 138 | + animation-delay: -4s; | ||
| 139 | + animation-duration: 6s; | ||
| 140 | + } | ||
| 141 | + | ||
| 142 | + .wave:nth-child(3) { | ||
| 143 | + animation-delay: -6s; | ||
| 144 | + animation-duration: 3s; | ||
| 145 | + } | ||
| 146 | + | ||
| 147 | + .waves-rect > g + rect { | ||
| 148 | + // transform: translateY( | ||
| 149 | + // calc(calc(100 - var(--value)) * var(--full-flag) * 1% + var(--full-flag) * 15%) | ||
| 150 | + // ); | ||
| 151 | + transition: transform linear 1s; | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + .height { | ||
| 155 | + // transform: translateY(calc(var(--value) * -1% - 10% + var(--over-min-flag) * 10%)); | ||
| 156 | + transition: transform linear 1s; | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + .waves-rect .text { | ||
| 160 | + display: flex; | ||
| 161 | + justify-content: center; | ||
| 162 | + align-items: center; | ||
| 163 | + width: 100%; | ||
| 164 | + height: 100%; | ||
| 165 | + color: #fff; | ||
| 166 | + font-weight: 700; | ||
| 167 | + } | ||
| 168 | +</style> |
| 1 | +import cloneDeep from 'lodash-es/cloneDeep'; | ||
| 2 | +import { DigitalDashboardComponentConfig } from '.'; | ||
| 3 | +import { | ||
| 4 | + ConfigType, | ||
| 5 | + CreateComponentType, | ||
| 6 | + PublicComponentOptions, | ||
| 7 | + PublicPresetOptions, | ||
| 8 | +} from '/@/views/visual/packages/index.type'; | ||
| 9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | ||
| 10 | +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 11 | + | ||
| 12 | +export const option: PublicPresetOptions = { | ||
| 13 | + [ComponentConfigFieldEnum.FONT_COLOR]: '#000', | ||
| 14 | + [ComponentConfigFieldEnum.UNIT]: 'kw/h', | ||
| 15 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false, | ||
| 16 | +}; | ||
| 17 | + | ||
| 18 | +export default class Config extends PublicConfigClass implements CreateComponentType { | ||
| 19 | + public key: string = DigitalDashboardComponentConfig.key; | ||
| 20 | + | ||
| 21 | + public attr = { ...componentInitConfig, w: 340 }; | ||
| 22 | + | ||
| 23 | + public componentConfig: ConfigType = cloneDeep(DigitalDashboardComponentConfig); | ||
| 24 | + | ||
| 25 | + public persetOption = cloneDeep(option); | ||
| 26 | + | ||
| 27 | + public option: PublicComponentOptions; | ||
| 28 | + | ||
| 29 | + constructor(option: PublicComponentOptions) { | ||
| 30 | + super(); | ||
| 31 | + this.option = { ...option }; | ||
| 32 | + } | ||
| 33 | +} |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 3 | + import { useForm, BasicForm } from '/@/components/Form'; | ||
| 4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 5 | + import { option } from './config'; | ||
| 6 | + | ||
| 7 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | ||
| 8 | + schemas: [ | ||
| 9 | + { | ||
| 10 | + field: ComponentConfigFieldEnum.FONT_COLOR, | ||
| 11 | + label: '数值字体颜色', | ||
| 12 | + component: 'ColorPicker', | ||
| 13 | + changeEvent: 'update:value', | ||
| 14 | + defaultValue: option.fontColor, | ||
| 15 | + }, | ||
| 16 | + { | ||
| 17 | + field: ComponentConfigFieldEnum.UNIT, | ||
| 18 | + label: '数值单位', | ||
| 19 | + component: 'Input', | ||
| 20 | + defaultValue: option.unit, | ||
| 21 | + componentProps: { | ||
| 22 | + placeholder: '请输入数值单位', | ||
| 23 | + }, | ||
| 24 | + }, | ||
| 25 | + { | ||
| 26 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | ||
| 27 | + label: '显示设备名称', | ||
| 28 | + component: 'Checkbox', | ||
| 29 | + defaultValue: option.showDeviceName, | ||
| 30 | + }, | ||
| 31 | + ], | ||
| 32 | + showActionButtonGroup: false, | ||
| 33 | + labelWidth: 120, | ||
| 34 | + baseColProps: { | ||
| 35 | + span: 12, | ||
| 36 | + }, | ||
| 37 | + }); | ||
| 38 | + | ||
| 39 | + const getFormValues = () => { | ||
| 40 | + return getFieldsValue(); | ||
| 41 | + }; | ||
| 42 | + | ||
| 43 | + const setFormValues = (data: Recordable) => { | ||
| 44 | + return setFieldsValue(data); | ||
| 45 | + }; | ||
| 46 | + | ||
| 47 | + defineExpose({ | ||
| 48 | + getFormValues, | ||
| 49 | + setFormValues, | ||
| 50 | + resetFormValues: resetFields, | ||
| 51 | + } as PublicFormInstaceType); | ||
| 52 | +</script> | ||
| 53 | + | ||
| 54 | +<template> | ||
| 55 | + <BasicForm @register="register" /> | ||
| 56 | +</template> |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { CreateComponentType } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
| 4 | + import { | ||
| 5 | + PublicComponentValueType, | ||
| 6 | + PublicFormInstaceType, | ||
| 7 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 8 | + import { commonDataSourceSchemas } from '../../../config/common.config'; | ||
| 9 | + | ||
| 10 | + defineProps<{ | ||
| 11 | + values: PublicComponentValueType; | ||
| 12 | + componentConfig: CreateComponentType; | ||
| 13 | + }>(); | ||
| 14 | + | ||
| 15 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | ||
| 16 | + labelWidth: 0, | ||
| 17 | + showActionButtonGroup: false, | ||
| 18 | + layout: 'horizontal', | ||
| 19 | + labelCol: { span: 0 }, | ||
| 20 | + schemas: commonDataSourceSchemas(), | ||
| 21 | + }); | ||
| 22 | + | ||
| 23 | + const getFormValues = () => { | ||
| 24 | + return getFieldsValue(); | ||
| 25 | + }; | ||
| 26 | + | ||
| 27 | + const setFormValues = (record: Recordable) => { | ||
| 28 | + return setFieldsValue(record); | ||
| 29 | + }; | ||
| 30 | + | ||
| 31 | + defineExpose({ | ||
| 32 | + getFormValues, | ||
| 33 | + setFormValues, | ||
| 34 | + validate, | ||
| 35 | + resetFormValues: resetFields, | ||
| 36 | + } as PublicFormInstaceType); | ||
| 37 | +</script> | ||
| 38 | + | ||
| 39 | +<template> | ||
| 40 | + <BasicForm @register="register" /> | ||
| 41 | +</template> |
| 1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | ||
| 2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | ||
| 3 | + | ||
| 4 | +const componentKeys = useComponentKeys('DigitalDashboardComponent'); | ||
| 5 | + | ||
| 6 | +export const DigitalDashboardComponentConfig: ConfigType = { | ||
| 7 | + ...componentKeys, | ||
| 8 | + title: '数字仪表盘', | ||
| 9 | + package: PackagesCategoryEnum.INSTRUMENT, | ||
| 10 | +}; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { option } from './config'; | ||
| 4 | + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket'; | ||
| 5 | + import { ref, computed, unref } from 'vue'; | ||
| 6 | + import { Space } from 'ant-design-vue'; | ||
| 7 | + import { useComponentScale } from '/@/views/visual/packages/hook/useComponentScale'; | ||
| 8 | + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime'; | ||
| 9 | + | ||
| 10 | + const props = defineProps<{ | ||
| 11 | + config: ComponentPropsConfigType<typeof option>; | ||
| 12 | + }>(); | ||
| 13 | + | ||
| 14 | + const currentValue = ref(99.23); | ||
| 15 | + | ||
| 16 | + const time = ref<Nullable<number>>(null); | ||
| 17 | + | ||
| 18 | + const integerPart = computed(() => { | ||
| 19 | + let number = unref(currentValue); | ||
| 20 | + const max = 5; | ||
| 21 | + if (isNaN(number)) number = 0; | ||
| 22 | + let _value = number.toString().split('.')[0]; | ||
| 23 | + | ||
| 24 | + if (_value.length > max) return ''.padStart(max, '9'); | ||
| 25 | + if (_value.length < max) return _value.padStart(max, '0'); | ||
| 26 | + | ||
| 27 | + return _value; | ||
| 28 | + }); | ||
| 29 | + | ||
| 30 | + const decimalPart = computed(() => { | ||
| 31 | + let number = unref(currentValue); | ||
| 32 | + | ||
| 33 | + const keepNumber = 2; | ||
| 34 | + | ||
| 35 | + if (isNaN(number)) number = 0; | ||
| 36 | + | ||
| 37 | + let _value = number.toString().split('.')[1] || '0'; | ||
| 38 | + | ||
| 39 | + if (_value.length < keepNumber) return ''.padStart(keepNumber, '0'); | ||
| 40 | + | ||
| 41 | + if (_value.length > keepNumber) return _value.slice(0, 2); | ||
| 42 | + | ||
| 43 | + return _value; | ||
| 44 | + }); | ||
| 45 | + | ||
| 46 | + const getDesign = computed(() => { | ||
| 47 | + const { option, persetOption } = props.config; | ||
| 48 | + const { componentInfo, attribute, attributeRename } = option; | ||
| 49 | + const { fontColor: presetFontColor, unit: presetUnit } = persetOption || {}; | ||
| 50 | + const { unit, fontColor } = componentInfo || {}; | ||
| 51 | + return { | ||
| 52 | + unit: unit ?? presetUnit, | ||
| 53 | + fontColor: fontColor ?? presetFontColor, | ||
| 54 | + attribute: attributeRename || attribute, | ||
| 55 | + }; | ||
| 56 | + }); | ||
| 57 | + | ||
| 58 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | ||
| 59 | + const { data = {} } = message; | ||
| 60 | + const [latest] = data[attribute] || []; | ||
| 61 | + const [timespan, value] = latest; | ||
| 62 | + time.value = timespan; | ||
| 63 | + currentValue.value = isNaN(value as unknown as number) ? 0 : Number(value); | ||
| 64 | + }; | ||
| 65 | + | ||
| 66 | + useDataFetch(props, updateFn); | ||
| 67 | + | ||
| 68 | + const { getScale } = useComponentScale(props); | ||
| 69 | +</script> | ||
| 70 | + | ||
| 71 | +<template> | ||
| 72 | + <main class="w-full h-full flex flex-col justify-center items-center"> | ||
| 73 | + <div class="flex flex-col w-full h-full"> | ||
| 74 | + <div class="flex-1 flex justify-center items-center"> | ||
| 75 | + <div class="flex px-4 items-center transform scale-75" :style="getScale"> | ||
| 76 | + <Space | ||
| 77 | + justify="end" | ||
| 78 | + class="justify-end" | ||
| 79 | + :size="4" | ||
| 80 | + :style="{ | ||
| 81 | + backgroundColor: '#585357', | ||
| 82 | + padding: '10px', | ||
| 83 | + }" | ||
| 84 | + > | ||
| 85 | + <div | ||
| 86 | + v-for="number in integerPart" | ||
| 87 | + :key="number" | ||
| 88 | + class="digital-wrapper__int" | ||
| 89 | + :style="{ | ||
| 90 | + color: getDesign.fontColor, | ||
| 91 | + }" | ||
| 92 | + > | ||
| 93 | + <div class="digital-text__int p-1 text-light-50"> {{ number }}</div> | ||
| 94 | + </div> | ||
| 95 | + </Space> | ||
| 96 | + <div | ||
| 97 | + class="m-0.5 rounded-1/2" | ||
| 98 | + style="background-color: #333; width: 6px; height: 6px; align-self: flex-end" | ||
| 99 | + > | ||
| 100 | + </div> | ||
| 101 | + <Space | ||
| 102 | + justify="end" | ||
| 103 | + class="justify-end" | ||
| 104 | + :size="4" | ||
| 105 | + :style="{ | ||
| 106 | + backgroundColor: '#b74940', | ||
| 107 | + padding: '10px', | ||
| 108 | + }" | ||
| 109 | + > | ||
| 110 | + <div | ||
| 111 | + v-for="number in decimalPart" | ||
| 112 | + :key="number" | ||
| 113 | + class="digital-wrapper__float" | ||
| 114 | + :style="{ | ||
| 115 | + color: getDesign.fontColor, | ||
| 116 | + }" | ||
| 117 | + > | ||
| 118 | + <div class="digital-text__float p-1 text-light-50"> | ||
| 119 | + {{ number }} | ||
| 120 | + </div> | ||
| 121 | + </div> | ||
| 122 | + </Space> | ||
| 123 | + <div class="px-1 font-bold"> | ||
| 124 | + {{ getDesign.unit || 'kw/h' }} | ||
| 125 | + </div> | ||
| 126 | + </div> | ||
| 127 | + </div> | ||
| 128 | + | ||
| 129 | + <div class="text-center truncate text-xs text-gray-500"> | ||
| 130 | + <span>{{ getDesign.attribute || '电表' }}</span> | ||
| 131 | + </div> | ||
| 132 | + | ||
| 133 | + <UpdateTime :time="time" /> | ||
| 134 | + </div> | ||
| 135 | + </main> | ||
| 136 | +</template> | ||
| 137 | + | ||
| 138 | +<style scoped lang="less"> | ||
| 139 | + .digital-wrapper__int { | ||
| 140 | + border-radius: 1px; | ||
| 141 | + box-shadow: inset 0 1px 3px 0 rgba(0, 0, 0, 0.7); | ||
| 142 | + background: url('/@/assets/images/digital-wrapper-bg-int.png') 0 -1px no-repeat; | ||
| 143 | + padding: 5px; | ||
| 144 | + background-size: 100% 100%; | ||
| 145 | + } | ||
| 146 | + | ||
| 147 | + .digital-text_int { | ||
| 148 | + display: inline-block; | ||
| 149 | + overflow-wrap: break-word; | ||
| 150 | + color: rgba(255, 255, 255, 1); | ||
| 151 | + white-space: nowrap; | ||
| 152 | + text-align: center; | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + .digital-wrapper__float { | ||
| 156 | + border-radius: 1px; | ||
| 157 | + box-shadow: inset 0 1px 3px 0 rgba(112, 22, 15, 1); | ||
| 158 | + background: url('/@/assets/images/digital-wrapper-bg-float.png') 0 -1px no-repeat; | ||
| 159 | + padding: 5px; | ||
| 160 | + background-size: 100% 100%; | ||
| 161 | + } | ||
| 162 | + | ||
| 163 | + .digital-text_float { | ||
| 164 | + display: inline-block; | ||
| 165 | + overflow-wrap: break-word; | ||
| 166 | + color: rgba(255, 255, 255, 1); | ||
| 167 | + white-space: nowrap; | ||
| 168 | + text-align: center; | ||
| 169 | + } | ||
| 170 | +</style> |
| 1 | +import cloneDeep from 'lodash-es/cloneDeep'; | ||
| 2 | +import { InstrumentComponent1Config } from '.'; | ||
| 3 | +import { | ||
| 4 | + ConfigType, | ||
| 5 | + CreateComponentType, | ||
| 6 | + PublicComponentOptions, | ||
| 7 | + PublicPresetOptions, | ||
| 8 | +} from '../../../index.type'; | ||
| 9 | +import { PublicConfigClass, componentInitConfig } from '../../../publicConfig'; | ||
| 10 | +import { ComponentConfigFieldEnum } from '../../../enum'; | ||
| 11 | + | ||
| 12 | +export const option: PublicPresetOptions = { | ||
| 13 | + [ComponentConfigFieldEnum.FONT_COLOR]: '#FD7347', | ||
| 14 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false, | ||
| 15 | + [ComponentConfigFieldEnum.UNIT]: '℃', | ||
| 16 | +}; | ||
| 17 | + | ||
| 18 | +export default class Config extends PublicConfigClass implements CreateComponentType { | ||
| 19 | + public key: string = InstrumentComponent1Config.key; | ||
| 20 | + | ||
| 21 | + public attr = { ...componentInitConfig }; | ||
| 22 | + | ||
| 23 | + public componentConfig: ConfigType = cloneDeep(InstrumentComponent1Config); | ||
| 24 | + | ||
| 25 | + public persetOption = cloneDeep(option); | ||
| 26 | + | ||
| 27 | + public option: PublicComponentOptions; | ||
| 28 | + | ||
| 29 | + constructor(option: PublicComponentOptions) { | ||
| 30 | + super(); | ||
| 31 | + this.option = { ...option }; | ||
| 32 | + } | ||
| 33 | +} |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentConfigFieldEnum } from '../../../enum'; | ||
| 3 | + import { option } from './config'; | ||
| 4 | + import { useForm, BasicForm } from '/@/components/Form'; | ||
| 5 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 6 | + | ||
| 7 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | ||
| 8 | + schemas: [ | ||
| 9 | + { | ||
| 10 | + field: ComponentConfigFieldEnum.FONT_COLOR, | ||
| 11 | + label: '数值字体颜色', | ||
| 12 | + component: 'ColorPicker', | ||
| 13 | + changeEvent: 'update:value', | ||
| 14 | + defaultValue: option.fontColor, | ||
| 15 | + }, | ||
| 16 | + { | ||
| 17 | + field: ComponentConfigFieldEnum.UNIT, | ||
| 18 | + label: '数值单位', | ||
| 19 | + component: 'Input', | ||
| 20 | + defaultValue: option.unit, | ||
| 21 | + }, | ||
| 22 | + { | ||
| 23 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | ||
| 24 | + label: '显示设备名称', | ||
| 25 | + component: 'Checkbox', | ||
| 26 | + defaultValue: option.showDeviceName, | ||
| 27 | + }, | ||
| 28 | + ], | ||
| 29 | + showActionButtonGroup: false, | ||
| 30 | + labelWidth: 120, | ||
| 31 | + baseColProps: { | ||
| 32 | + span: 12, | ||
| 33 | + }, | ||
| 34 | + }); | ||
| 35 | + | ||
| 36 | + const getFormValues = () => { | ||
| 37 | + return getFieldsValue(); | ||
| 38 | + }; | ||
| 39 | + | ||
| 40 | + const setFormValues = (data: Recordable) => { | ||
| 41 | + return setFieldsValue(data); | ||
| 42 | + }; | ||
| 43 | + | ||
| 44 | + defineExpose({ | ||
| 45 | + getFormValues, | ||
| 46 | + setFormValues, | ||
| 47 | + resetFormValues: resetFields, | ||
| 48 | + } as PublicFormInstaceType); | ||
| 49 | +</script> | ||
| 50 | + | ||
| 51 | +<template> | ||
| 52 | + <BasicForm @register="register" /> | ||
| 53 | +</template> |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { commonDataSourceSchemas } from '../../../config/common.config'; | ||
| 3 | + import { CreateComponentType } from '../../../index.type'; | ||
| 4 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
| 5 | + import { | ||
| 6 | + PublicComponentValueType, | ||
| 7 | + PublicFormInstaceType, | ||
| 8 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 9 | + | ||
| 10 | + defineProps<{ | ||
| 11 | + values: PublicComponentValueType; | ||
| 12 | + componentConfig: CreateComponentType; | ||
| 13 | + }>(); | ||
| 14 | + | ||
| 15 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | ||
| 16 | + labelWidth: 0, | ||
| 17 | + showActionButtonGroup: false, | ||
| 18 | + layout: 'horizontal', | ||
| 19 | + labelCol: { span: 0 }, | ||
| 20 | + schemas: commonDataSourceSchemas(), | ||
| 21 | + }); | ||
| 22 | + | ||
| 23 | + const getFormValues = () => { | ||
| 24 | + return getFieldsValue(); | ||
| 25 | + }; | ||
| 26 | + | ||
| 27 | + const setFormValues = (record: Recordable) => { | ||
| 28 | + return setFieldsValue(record); | ||
| 29 | + }; | ||
| 30 | + | ||
| 31 | + defineExpose({ | ||
| 32 | + getFormValues, | ||
| 33 | + setFormValues, | ||
| 34 | + validate, | ||
| 35 | + resetFormValues: resetFields, | ||
| 36 | + } as PublicFormInstaceType); | ||
| 37 | +</script> | ||
| 38 | + | ||
| 39 | +<template> | ||
| 40 | + <BasicForm @register="register" /> | ||
| 41 | +</template> |
| 1 | +import { useComponentKeys } from '../../../hook/useComponentKeys'; | ||
| 2 | +import { ConfigType, PackagesCategoryEnum } from '../../../index.type'; | ||
| 3 | + | ||
| 4 | +const componentKeys = useComponentKeys('InstrumentComponent1'); | ||
| 5 | + | ||
| 6 | +export const InstrumentComponent1Config: ConfigType = { | ||
| 7 | + ...componentKeys, | ||
| 8 | + title: '仪表盘', | ||
| 9 | + package: PackagesCategoryEnum.INSTRUMENT, | ||
| 10 | +}; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { EChartsOption, ECharts, init } from 'echarts'; | ||
| 3 | + import { onMounted } from 'vue'; | ||
| 4 | + import { unref } from 'vue'; | ||
| 5 | + import { ref } from 'vue'; | ||
| 6 | + import { useDataFetch } from '../../../hook/useSocket'; | ||
| 7 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '../../../index.type'; | ||
| 8 | + import { option } from './config'; | ||
| 9 | + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime'; | ||
| 10 | + import { useIntervalFn } from '@vueuse/core'; | ||
| 11 | + import { computed } from 'vue'; | ||
| 12 | + import { useComponentScale } from '../../../hook/useComponentScale'; | ||
| 13 | + import { nextTick } from 'vue'; | ||
| 14 | + import { DeviceName } from '/@/views/visual/commonComponents/DeviceName'; | ||
| 15 | + | ||
| 16 | + const props = defineProps<{ | ||
| 17 | + config: ComponentPropsConfigType<typeof option>; | ||
| 18 | + }>(); | ||
| 19 | + | ||
| 20 | + const chartRefEl = ref<Nullable<HTMLDivElement>>(null); | ||
| 21 | + | ||
| 22 | + const chartInstance = ref<Nullable<ECharts>>(null); | ||
| 23 | + | ||
| 24 | + const time = ref<Nullable<number>>(null); | ||
| 25 | + | ||
| 26 | + const getDesign = computed(() => { | ||
| 27 | + const { option, persetOption } = props.config; | ||
| 28 | + const { componentInfo, attribute, attributeRename } = option; | ||
| 29 | + const { fontColor: presetFontColor, unit: presetUnit } = persetOption || {}; | ||
| 30 | + const { unit, fontColor } = componentInfo || {}; | ||
| 31 | + return { | ||
| 32 | + unit: unit ?? presetUnit, | ||
| 33 | + fontColor: fontColor ?? presetFontColor, | ||
| 34 | + attribute: attributeRename || attribute, | ||
| 35 | + }; | ||
| 36 | + }); | ||
| 37 | + | ||
| 38 | + const options = (): EChartsOption => { | ||
| 39 | + const { unit, fontColor } = unref(getDesign); | ||
| 40 | + return { | ||
| 41 | + series: [ | ||
| 42 | + { | ||
| 43 | + type: 'gauge', | ||
| 44 | + radius: '50%', | ||
| 45 | + center: ['50%', '60%'], | ||
| 46 | + startAngle: 200, | ||
| 47 | + endAngle: -20, | ||
| 48 | + min: 0, | ||
| 49 | + max: 100, | ||
| 50 | + splitNumber: 10, | ||
| 51 | + itemStyle: { | ||
| 52 | + color: fontColor, | ||
| 53 | + }, | ||
| 54 | + progress: { | ||
| 55 | + show: true, | ||
| 56 | + width: 30, | ||
| 57 | + }, | ||
| 58 | + pointer: { | ||
| 59 | + show: false, | ||
| 60 | + }, | ||
| 61 | + axisLine: { | ||
| 62 | + lineStyle: { | ||
| 63 | + width: 30, | ||
| 64 | + }, | ||
| 65 | + }, | ||
| 66 | + axisTick: { | ||
| 67 | + distance: -35, | ||
| 68 | + splitNumber: 5, | ||
| 69 | + lineStyle: { | ||
| 70 | + width: 2, | ||
| 71 | + color: '#999', | ||
| 72 | + }, | ||
| 73 | + }, | ||
| 74 | + splitLine: { | ||
| 75 | + distance: -40, | ||
| 76 | + length: 10, | ||
| 77 | + lineStyle: { | ||
| 78 | + width: 3, | ||
| 79 | + color: '#999', | ||
| 80 | + }, | ||
| 81 | + }, | ||
| 82 | + axisLabel: { | ||
| 83 | + distance: 0, | ||
| 84 | + color: '#999', | ||
| 85 | + }, | ||
| 86 | + anchor: { | ||
| 87 | + show: false, | ||
| 88 | + }, | ||
| 89 | + title: { | ||
| 90 | + show: false, | ||
| 91 | + }, | ||
| 92 | + detail: { | ||
| 93 | + valueAnimation: true, | ||
| 94 | + width: '60%', | ||
| 95 | + lineHeight: 10, | ||
| 96 | + borderRadius: 8, | ||
| 97 | + offsetCenter: [0, '30%'], | ||
| 98 | + fontSize: 14, | ||
| 99 | + fontWeight: 'bolder', | ||
| 100 | + formatter: `{value} ${unit ?? ''}`, | ||
| 101 | + color: fontColor || 'inherit', | ||
| 102 | + }, | ||
| 103 | + data: [ | ||
| 104 | + { | ||
| 105 | + value: 20, | ||
| 106 | + }, | ||
| 107 | + ], | ||
| 108 | + }, | ||
| 109 | + { | ||
| 110 | + type: 'gauge', | ||
| 111 | + radius: '50%', | ||
| 112 | + center: ['50%', '60%'], | ||
| 113 | + startAngle: 200, | ||
| 114 | + endAngle: -20, | ||
| 115 | + min: 0, | ||
| 116 | + max: 100, | ||
| 117 | + itemStyle: { | ||
| 118 | + color: fontColor, | ||
| 119 | + }, | ||
| 120 | + progress: { | ||
| 121 | + show: true, | ||
| 122 | + width: 8, | ||
| 123 | + }, | ||
| 124 | + pointer: { | ||
| 125 | + show: false, | ||
| 126 | + }, | ||
| 127 | + axisLine: { | ||
| 128 | + show: false, | ||
| 129 | + }, | ||
| 130 | + axisTick: { | ||
| 131 | + show: false, | ||
| 132 | + }, | ||
| 133 | + splitLine: { | ||
| 134 | + show: false, | ||
| 135 | + }, | ||
| 136 | + axisLabel: { | ||
| 137 | + show: false, | ||
| 138 | + }, | ||
| 139 | + detail: { | ||
| 140 | + show: false, | ||
| 141 | + }, | ||
| 142 | + data: [ | ||
| 143 | + { | ||
| 144 | + value: 20, | ||
| 145 | + }, | ||
| 146 | + ], | ||
| 147 | + }, | ||
| 148 | + ], | ||
| 149 | + }; | ||
| 150 | + }; | ||
| 151 | + | ||
| 152 | + const updateChart = (value: number) => { | ||
| 153 | + unref(chartInstance)?.setOption({ | ||
| 154 | + series: [{ data: [{ value: value.toFixed(2) }] }, { data: [{ value: value.toFixed(2) }] }], | ||
| 155 | + } as EChartsOption); | ||
| 156 | + }; | ||
| 157 | + | ||
| 158 | + const initial = () => { | ||
| 159 | + chartInstance.value = init(unref(chartRefEl)!); | ||
| 160 | + chartInstance.value.setOption(options()); | ||
| 161 | + }; | ||
| 162 | + | ||
| 163 | + const randomFn = () => { | ||
| 164 | + useIntervalFn(() => { | ||
| 165 | + const value = (Math.random() * 100).toFixed(0); | ||
| 166 | + unref(chartInstance)?.setOption({ | ||
| 167 | + series: [{ data: [{ value }] }, { data: [{ value }] }], | ||
| 168 | + } as EChartsOption); | ||
| 169 | + }, 3000); | ||
| 170 | + }; | ||
| 171 | + | ||
| 172 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | ||
| 173 | + const { data = {} } = message; | ||
| 174 | + const [latest] = data[attribute] || []; | ||
| 175 | + const [timespan, value] = latest; | ||
| 176 | + time.value = timespan; | ||
| 177 | + updateChart(isNaN(value as unknown as number) ? 0 : Number(value)); | ||
| 178 | + }; | ||
| 179 | + | ||
| 180 | + useDataFetch(props, updateFn); | ||
| 181 | + | ||
| 182 | + onMounted(() => { | ||
| 183 | + initial(); | ||
| 184 | + !props.config.option.uuid && randomFn(); | ||
| 185 | + }); | ||
| 186 | + | ||
| 187 | + const resize = async () => { | ||
| 188 | + await nextTick(); | ||
| 189 | + unref(chartInstance)?.resize(); | ||
| 190 | + }; | ||
| 191 | + | ||
| 192 | + useComponentScale(props, resize); | ||
| 193 | +</script> | ||
| 194 | + | ||
| 195 | +<template> | ||
| 196 | + <main class="w-full h-full flex flex-col justify-center items-center"> | ||
| 197 | + <DeviceName :config="config" /> | ||
| 198 | + <div ref="chartRefEl" class="flex-1 w-full h-full"> </div> | ||
| 199 | + <div class="text-gray-500 text-xs text-center truncate">{{ | ||
| 200 | + getDesign.attribute || '温度' | ||
| 201 | + }}</div> | ||
| 202 | + <UpdateTime :time="time" /> | ||
| 203 | + </main> | ||
| 204 | +</template> |
| 1 | +import cloneDeep from 'lodash-es/cloneDeep'; | ||
| 2 | +import { InstrumentComponent2Config } from '.'; | ||
| 3 | +import { | ||
| 4 | + ConfigType, | ||
| 5 | + CreateComponentType, | ||
| 6 | + PublicComponentOptions, | ||
| 7 | + PublicPresetOptions, | ||
| 8 | +} from '/@/views/visual/packages/index.type'; | ||
| 9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | ||
| 10 | +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 11 | + | ||
| 12 | +export enum Gradient { | ||
| 13 | + FIRST = 'first', | ||
| 14 | + SECOND = 'second', | ||
| 15 | + THIRD = 'third', | ||
| 16 | +} | ||
| 17 | +export enum GradientColor { | ||
| 18 | + FIRST = '#67e0e3', | ||
| 19 | + SECOND = '#37a2da', | ||
| 20 | + THIRD = '#fd666d', | ||
| 21 | +} | ||
| 22 | + | ||
| 23 | +export const option: PublicPresetOptions = { | ||
| 24 | + [ComponentConfigFieldEnum.FONT_COLOR]: GradientColor.THIRD, | ||
| 25 | + [ComponentConfigFieldEnum.GRADIENT_INFO]: [ | ||
| 26 | + { key: Gradient.FIRST, value: 30, color: GradientColor.FIRST }, | ||
| 27 | + { key: Gradient.SECOND, value: 70, color: GradientColor.SECOND }, | ||
| 28 | + { key: Gradient.THIRD, value: 100, color: GradientColor.THIRD }, | ||
| 29 | + ], | ||
| 30 | + [ComponentConfigFieldEnum.UNIT]: 'km/h', | ||
| 31 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false, | ||
| 32 | +}; | ||
| 33 | + | ||
| 34 | +export default class Config extends PublicConfigClass implements CreateComponentType { | ||
| 35 | + public key: string = InstrumentComponent2Config.key; | ||
| 36 | + | ||
| 37 | + public attr = { ...componentInitConfig }; | ||
| 38 | + | ||
| 39 | + public componentConfig: ConfigType = cloneDeep(InstrumentComponent2Config); | ||
| 40 | + | ||
| 41 | + public persetOption = cloneDeep(option); | ||
| 42 | + | ||
| 43 | + public option: PublicComponentOptions; | ||
| 44 | + | ||
| 45 | + constructor(option: PublicComponentOptions) { | ||
| 46 | + super(); | ||
| 47 | + this.option = { ...option }; | ||
| 48 | + } | ||
| 49 | +} |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 3 | + import { useForm, BasicForm } from '/@/components/Form'; | ||
| 4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 5 | + import { Gradient, GradientColor, option } from './config'; | ||
| 6 | + import { ComponentInfo } from '/@/views/visual/palette/types'; | ||
| 7 | + | ||
| 8 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | ||
| 9 | + schemas: [ | ||
| 10 | + { | ||
| 11 | + field: ComponentConfigFieldEnum.FONT_COLOR, | ||
| 12 | + label: '数值字体颜色', | ||
| 13 | + component: 'ColorPicker', | ||
| 14 | + changeEvent: 'update:value', | ||
| 15 | + defaultValue: option.fontColor, | ||
| 16 | + }, | ||
| 17 | + { | ||
| 18 | + field: ComponentConfigFieldEnum.UNIT, | ||
| 19 | + label: '数值单位', | ||
| 20 | + component: 'Input', | ||
| 21 | + defaultValue: option.unit, | ||
| 22 | + componentProps: { | ||
| 23 | + placeholder: '请输入数值单位', | ||
| 24 | + }, | ||
| 25 | + }, | ||
| 26 | + { | ||
| 27 | + field: ComponentConfigFieldEnum.FIRST_PHASE_COLOR, | ||
| 28 | + label: '一阶段颜色', | ||
| 29 | + component: 'ColorPicker', | ||
| 30 | + changeEvent: 'update:value', | ||
| 31 | + defaultValue: GradientColor.FIRST, | ||
| 32 | + }, | ||
| 33 | + { | ||
| 34 | + field: ComponentConfigFieldEnum.FIRST_PHASE_VALUE, | ||
| 35 | + label: '一阶段阀值', | ||
| 36 | + component: 'InputNumber', | ||
| 37 | + componentProps: { | ||
| 38 | + placeholder: '请输入一阶段阀值', | ||
| 39 | + min: 0, | ||
| 40 | + }, | ||
| 41 | + }, | ||
| 42 | + { | ||
| 43 | + field: ComponentConfigFieldEnum.SECOND_PHASE_COLOR, | ||
| 44 | + label: '二阶段颜色', | ||
| 45 | + component: 'ColorPicker', | ||
| 46 | + changeEvent: 'update:value', | ||
| 47 | + defaultValue: GradientColor.SECOND, | ||
| 48 | + }, | ||
| 49 | + { | ||
| 50 | + field: ComponentConfigFieldEnum.SECOND_PHASE_VALUE, | ||
| 51 | + label: '二阶段阀值', | ||
| 52 | + component: 'InputNumber', | ||
| 53 | + componentProps: ({ formModel }) => { | ||
| 54 | + return { | ||
| 55 | + placeholder: '请输入二阶段阀值', | ||
| 56 | + min: formModel[ComponentConfigFieldEnum.FIRST_PHASE_VALUE], | ||
| 57 | + }; | ||
| 58 | + }, | ||
| 59 | + }, | ||
| 60 | + { | ||
| 61 | + field: ComponentConfigFieldEnum.THIRD_PHASE_COLOR, | ||
| 62 | + label: '三阶段颜色', | ||
| 63 | + component: 'ColorPicker', | ||
| 64 | + changeEvent: 'update:value', | ||
| 65 | + defaultValue: GradientColor.THIRD, | ||
| 66 | + }, | ||
| 67 | + { | ||
| 68 | + field: ComponentConfigFieldEnum.THIRD_PHASE_VALUE, | ||
| 69 | + label: '三阶段阀值', | ||
| 70 | + component: 'InputNumber', | ||
| 71 | + componentProps: ({ formModel }) => { | ||
| 72 | + return { | ||
| 73 | + placeholder: '请输入三阶段阀值', | ||
| 74 | + min: formModel[ComponentConfigFieldEnum.SECOND_PHASE_VALUE], | ||
| 75 | + }; | ||
| 76 | + }, | ||
| 77 | + }, | ||
| 78 | + { | ||
| 79 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | ||
| 80 | + label: '显示设备名称', | ||
| 81 | + component: 'Checkbox', | ||
| 82 | + defaultValue: option.showDeviceName, | ||
| 83 | + }, | ||
| 84 | + ], | ||
| 85 | + showActionButtonGroup: false, | ||
| 86 | + labelWidth: 120, | ||
| 87 | + baseColProps: { | ||
| 88 | + span: 12, | ||
| 89 | + }, | ||
| 90 | + }); | ||
| 91 | + | ||
| 92 | + const getFormValues = () => { | ||
| 93 | + const value = getFieldsValue(); | ||
| 94 | + return { | ||
| 95 | + gradientInfo: [ | ||
| 96 | + { | ||
| 97 | + key: Gradient.FIRST, | ||
| 98 | + value: value[ComponentConfigFieldEnum.FIRST_PHASE_VALUE], | ||
| 99 | + color: value[ComponentConfigFieldEnum.FIRST_PHASE_COLOR], | ||
| 100 | + }, | ||
| 101 | + { | ||
| 102 | + key: Gradient.SECOND, | ||
| 103 | + value: value[ComponentConfigFieldEnum.SECOND_PHASE_VALUE], | ||
| 104 | + color: value[ComponentConfigFieldEnum.SECOND_PHASE_COLOR], | ||
| 105 | + }, | ||
| 106 | + { | ||
| 107 | + key: Gradient.THIRD, | ||
| 108 | + value: value[ComponentConfigFieldEnum.THIRD_PHASE_VALUE], | ||
| 109 | + color: value[ComponentConfigFieldEnum.THIRD_PHASE_COLOR], | ||
| 110 | + }, | ||
| 111 | + ], | ||
| 112 | + fontColor: value[ComponentConfigFieldEnum.FONT_COLOR], | ||
| 113 | + unit: value[ComponentConfigFieldEnum.UNIT], | ||
| 114 | + showDeviceName: value[ComponentConfigFieldEnum.SHOW_DEVICE_NAME], | ||
| 115 | + } as ComponentInfo; | ||
| 116 | + }; | ||
| 117 | + | ||
| 118 | + const setFormValues = (data: ComponentInfo) => { | ||
| 119 | + const { gradientInfo, unit, fontColor, showDeviceName } = data; | ||
| 120 | + const firstRecord = gradientInfo.find((item) => item.key === Gradient.FIRST); | ||
| 121 | + const secondRecord = gradientInfo.find((item) => item.key === Gradient.SECOND); | ||
| 122 | + const thirdRecord = gradientInfo.find((item) => item.key === Gradient.THIRD); | ||
| 123 | + const value = { | ||
| 124 | + [ComponentConfigFieldEnum.UNIT]: unit, | ||
| 125 | + [ComponentConfigFieldEnum.FONT_COLOR]: fontColor, | ||
| 126 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: showDeviceName, | ||
| 127 | + [ComponentConfigFieldEnum.FIRST_PHASE_VALUE]: firstRecord?.value, | ||
| 128 | + [ComponentConfigFieldEnum.FIRST_PHASE_COLOR]: firstRecord?.color, | ||
| 129 | + [ComponentConfigFieldEnum.SECOND_PHASE_VALUE]: secondRecord?.value, | ||
| 130 | + [ComponentConfigFieldEnum.SECOND_PHASE_COLOR]: secondRecord?.color, | ||
| 131 | + [ComponentConfigFieldEnum.THIRD_PHASE_VALUE]: thirdRecord?.value, | ||
| 132 | + [ComponentConfigFieldEnum.THIRD_PHASE_COLOR]: thirdRecord?.color, | ||
| 133 | + }; | ||
| 134 | + return setFieldsValue(value); | ||
| 135 | + }; | ||
| 136 | + | ||
| 137 | + defineExpose({ | ||
| 138 | + getFormValues, | ||
| 139 | + setFormValues, | ||
| 140 | + resetFormValues: resetFields, | ||
| 141 | + } as PublicFormInstaceType); | ||
| 142 | +</script> | ||
| 143 | + | ||
| 144 | +<template> | ||
| 145 | + <BasicForm @register="register" /> | ||
| 146 | +</template> |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { CreateComponentType } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
| 4 | + import { | ||
| 5 | + PublicComponentValueType, | ||
| 6 | + PublicFormInstaceType, | ||
| 7 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 8 | + import { commonDataSourceSchemas } from '../../../config/common.config'; | ||
| 9 | + | ||
| 10 | + defineProps<{ | ||
| 11 | + values: PublicComponentValueType; | ||
| 12 | + componentConfig: CreateComponentType; | ||
| 13 | + }>(); | ||
| 14 | + | ||
| 15 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | ||
| 16 | + labelWidth: 0, | ||
| 17 | + showActionButtonGroup: false, | ||
| 18 | + layout: 'horizontal', | ||
| 19 | + labelCol: { span: 0 }, | ||
| 20 | + schemas: commonDataSourceSchemas(), | ||
| 21 | + }); | ||
| 22 | + | ||
| 23 | + const getFormValues = () => { | ||
| 24 | + return getFieldsValue(); | ||
| 25 | + }; | ||
| 26 | + | ||
| 27 | + const setFormValues = (record: Recordable) => { | ||
| 28 | + return setFieldsValue(record); | ||
| 29 | + }; | ||
| 30 | + | ||
| 31 | + defineExpose({ | ||
| 32 | + getFormValues, | ||
| 33 | + setFormValues, | ||
| 34 | + validate, | ||
| 35 | + resetFormValues: resetFields, | ||
| 36 | + } as PublicFormInstaceType); | ||
| 37 | +</script> | ||
| 38 | + | ||
| 39 | +<template> | ||
| 40 | + <BasicForm @register="register" /> | ||
| 41 | +</template> |
| 1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | ||
| 2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | ||
| 3 | + | ||
| 4 | +const componentKeys = useComponentKeys('InstrumentComponent2'); | ||
| 5 | + | ||
| 6 | +export const InstrumentComponent2Config: ConfigType = { | ||
| 7 | + ...componentKeys, | ||
| 8 | + title: '阶段仪表盘', | ||
| 9 | + package: PackagesCategoryEnum.INSTRUMENT, | ||
| 10 | +}; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { Gradient, GradientColor, option } from './config'; | ||
| 4 | + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket'; | ||
| 5 | + import { ECharts, EChartsOption, init } from 'echarts'; | ||
| 6 | + import { ref, unref, onMounted, computed } from 'vue'; | ||
| 7 | + import { isArray } from '/@/utils/is'; | ||
| 8 | + import { ComponentInfoGradientInfoType } from '/@/views/visual/palette/types'; | ||
| 9 | + import { useComponentScale } from '/@/views/visual/packages/hook/useComponentScale'; | ||
| 10 | + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime'; | ||
| 11 | + import { useIntervalFn } from '@vueuse/core'; | ||
| 12 | + import { nextTick } from 'vue'; | ||
| 13 | + | ||
| 14 | + const props = defineProps<{ | ||
| 15 | + config: ComponentPropsConfigType<typeof option>; | ||
| 16 | + }>(); | ||
| 17 | + | ||
| 18 | + const time = ref<Nullable<number>>(null); | ||
| 19 | + | ||
| 20 | + useComponentScale(props, async () => { | ||
| 21 | + await nextTick(); | ||
| 22 | + unref(chartInstance)?.resize(); | ||
| 23 | + }); | ||
| 24 | + | ||
| 25 | + const chartRefEl = ref<Nullable<HTMLDivElement>>(null); | ||
| 26 | + | ||
| 27 | + const chartInstance = ref<Nullable<ECharts>>(null); | ||
| 28 | + | ||
| 29 | + const getDesign = computed(() => { | ||
| 30 | + const { option, persetOption } = props.config; | ||
| 31 | + const { componentInfo, attributeRename, attribute } = option; | ||
| 32 | + | ||
| 33 | + const { | ||
| 34 | + fontColor: presetFontColor, | ||
| 35 | + unit: presetUnit, | ||
| 36 | + gradientInfo: presetGradientInfo, | ||
| 37 | + } = persetOption || {}; | ||
| 38 | + | ||
| 39 | + const { unit, fontColor, gradientInfo } = componentInfo || {}; | ||
| 40 | + return { | ||
| 41 | + unit: unit ?? presetUnit, | ||
| 42 | + fontColor: fontColor ?? presetFontColor, | ||
| 43 | + gradientInfo: gradientInfo ?? presetGradientInfo, | ||
| 44 | + attribute: attributeRename || attribute, | ||
| 45 | + }; | ||
| 46 | + }); | ||
| 47 | + | ||
| 48 | + const getGradient = (key: Gradient, record: ComponentInfoGradientInfoType[] = []) => { | ||
| 49 | + if (!isArray(record)) return; | ||
| 50 | + return record.find((item) => item.key === key); | ||
| 51 | + }; | ||
| 52 | + | ||
| 53 | + const options = (): EChartsOption => { | ||
| 54 | + const { gradientInfo, unit, fontColor } = unref(getDesign); | ||
| 55 | + const firstRecord = getGradient(Gradient.FIRST, gradientInfo); | ||
| 56 | + const secondRecord = getGradient(Gradient.SECOND, gradientInfo); | ||
| 57 | + const thirdRecord = getGradient(Gradient.THIRD, gradientInfo); | ||
| 58 | + | ||
| 59 | + let max = thirdRecord?.value || secondRecord?.value || firstRecord?.value || 100; | ||
| 60 | + | ||
| 61 | + max = Number( | ||
| 62 | + 1 + | ||
| 63 | + Array(String(max).length - 1) | ||
| 64 | + .fill(0) | ||
| 65 | + .join('') | ||
| 66 | + ); | ||
| 67 | + | ||
| 68 | + const firstGradient = firstRecord?.value ? firstRecord.value / max : 0.3; | ||
| 69 | + const secondGradient = secondRecord?.value ? secondRecord.value / max : 0.7; | ||
| 70 | + | ||
| 71 | + return { | ||
| 72 | + series: [ | ||
| 73 | + { | ||
| 74 | + type: 'gauge', | ||
| 75 | + min: 0, | ||
| 76 | + max, | ||
| 77 | + axisLine: { | ||
| 78 | + lineStyle: { | ||
| 79 | + width: 20, | ||
| 80 | + color: [ | ||
| 81 | + [firstGradient, firstRecord?.color || GradientColor.FIRST], | ||
| 82 | + [secondGradient, secondRecord?.color || GradientColor.SECOND], | ||
| 83 | + [1, thirdRecord?.color || GradientColor.THIRD], | ||
| 84 | + ], | ||
| 85 | + }, | ||
| 86 | + }, | ||
| 87 | + pointer: { | ||
| 88 | + itemStyle: { | ||
| 89 | + color: 'inherit', | ||
| 90 | + }, | ||
| 91 | + }, | ||
| 92 | + axisTick: { | ||
| 93 | + distance: -30, | ||
| 94 | + length: 8, | ||
| 95 | + splitNumber: max / 100, | ||
| 96 | + lineStyle: { | ||
| 97 | + color: '#fff', | ||
| 98 | + width: 2, | ||
| 99 | + }, | ||
| 100 | + }, | ||
| 101 | + splitLine: { | ||
| 102 | + distance: -10, | ||
| 103 | + length: 30, | ||
| 104 | + lineStyle: { | ||
| 105 | + color: '#fff', | ||
| 106 | + width: 4, | ||
| 107 | + }, | ||
| 108 | + }, | ||
| 109 | + axisLabel: { | ||
| 110 | + color: 'inherit', | ||
| 111 | + distance: 5, | ||
| 112 | + fontSize: 6, | ||
| 113 | + }, | ||
| 114 | + detail: { | ||
| 115 | + valueAnimation: true, | ||
| 116 | + formatter: `{value} ${unit ?? ''}`, | ||
| 117 | + color: fontColor || 'inherit', | ||
| 118 | + offsetCenter: [0, '70%'], | ||
| 119 | + fontSize: 14, | ||
| 120 | + }, | ||
| 121 | + data: [ | ||
| 122 | + { | ||
| 123 | + value: 20, | ||
| 124 | + }, | ||
| 125 | + ], | ||
| 126 | + }, | ||
| 127 | + ], | ||
| 128 | + }; | ||
| 129 | + }; | ||
| 130 | + | ||
| 131 | + const updateChartFn = (value: number) => { | ||
| 132 | + unref(chartInstance)?.setOption({ | ||
| 133 | + series: [{ data: [{ value: value.toFixed(2) }] }], | ||
| 134 | + } as EChartsOption); | ||
| 135 | + }; | ||
| 136 | + | ||
| 137 | + const initial = () => { | ||
| 138 | + chartInstance.value = init(unref(chartRefEl)!); | ||
| 139 | + chartInstance.value.setOption(options()); | ||
| 140 | + }; | ||
| 141 | + | ||
| 142 | + const randomFn = () => { | ||
| 143 | + useIntervalFn(() => { | ||
| 144 | + const value = (Math.random() * 100).toFixed(0); | ||
| 145 | + unref(chartInstance)?.setOption({ | ||
| 146 | + series: [{ data: [{ value }] }], | ||
| 147 | + } as EChartsOption); | ||
| 148 | + }, 3000); | ||
| 149 | + }; | ||
| 150 | + | ||
| 151 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | ||
| 152 | + const { data = {} } = message; | ||
| 153 | + const [latest] = data[attribute] || []; | ||
| 154 | + const [timespan, value] = latest; | ||
| 155 | + time.value = timespan; | ||
| 156 | + updateChartFn(isNaN(value as unknown as number) ? 0 : Number(value)); | ||
| 157 | + }; | ||
| 158 | + | ||
| 159 | + useDataFetch(props, updateFn); | ||
| 160 | + | ||
| 161 | + onMounted(() => { | ||
| 162 | + initial(); | ||
| 163 | + !props.config.option.uuid && randomFn(); | ||
| 164 | + }); | ||
| 165 | +</script> | ||
| 166 | + | ||
| 167 | +<template> | ||
| 168 | + <main class="w-full h-full flex flex-col justify-center items-center"> | ||
| 169 | + <div ref="chartRefEl" class="flex-1 w-full h-full"> </div> | ||
| 170 | + <div class="text-center text-gray-500 text-xs truncate"> | ||
| 171 | + {{ getDesign.attribute || '速度' }} | ||
| 172 | + </div> | ||
| 173 | + <UpdateTime :time="time" /> | ||
| 174 | + </main> | ||
| 175 | +</template> |
| 1 | +import { DigitalDashboardComponentConfig } from './DigitalDashboardComponent'; | ||
| 2 | +import { InstrumentComponent1Config } from './InstrumentComponent1'; | ||
| 3 | +import { InstrumentComponent2Config } from './InstrumentComponent2'; | ||
| 4 | + | ||
| 5 | +export const InstrumentList = [ | ||
| 6 | + InstrumentComponent1Config, | ||
| 7 | + InstrumentComponent2Config, | ||
| 8 | + DigitalDashboardComponentConfig, | ||
| 9 | +]; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
| 3 | + import { BasicModal, useModalInner } from '/@/components/Modal'; | ||
| 4 | + import { formSchema, getHistorySearchParams, SchemaFiled } from './history.config'; | ||
| 5 | + import { HistoryModalOkEmitParams } from './type'; | ||
| 6 | + import { ref } from 'vue'; | ||
| 7 | + import { getAllDeviceByOrg } from '/@/api/dataBoard'; | ||
| 8 | + import { getDeviceHistoryInfo } from '/@/api/alarm/position'; | ||
| 9 | + import { DataSource } from '/@/views/visual/palette/types'; | ||
| 10 | + import { cloneDeep } from 'lodash-es'; | ||
| 11 | + | ||
| 12 | + const emit = defineEmits(['register', 'ok']); | ||
| 13 | + | ||
| 14 | + const [registerForm, { updateSchema, setFieldsValue, validate, getFieldsValue }] = useForm({ | ||
| 15 | + schemas: formSchema(), | ||
| 16 | + showActionButtonGroup: false, | ||
| 17 | + fieldMapToTime: [ | ||
| 18 | + [SchemaFiled.DATE_RANGE, [SchemaFiled.START_TS, SchemaFiled.END_TS], 'YYYY-MM-DD HH:mm:ss'], | ||
| 19 | + ], | ||
| 20 | + }); | ||
| 21 | + | ||
| 22 | + const [registerModal, { closeModal }] = useModalInner(async (dataSource: DataSource[]) => { | ||
| 23 | + try { | ||
| 24 | + dataSource = cloneDeep(dataSource); | ||
| 25 | + if (dataSource.length < 2) return; | ||
| 26 | + dataSource = dataSource.splice(0, 2); | ||
| 27 | + const deviceRecord = dataSource?.at(0) || ({} as DataSource); | ||
| 28 | + if (!deviceRecord.organizationId) return; | ||
| 29 | + const deviceList = await getAllDeviceByOrg( | ||
| 30 | + deviceRecord.organizationId, | ||
| 31 | + deviceRecord.deviceProfileId | ||
| 32 | + ); | ||
| 33 | + const options = deviceList | ||
| 34 | + .filter((item) => item.tbDeviceId === deviceRecord.deviceId) | ||
| 35 | + .map((item) => ({ ...item, label: item.name, value: item.tbDeviceId })); | ||
| 36 | + | ||
| 37 | + const attKey = dataSource.map((item) => ({ | ||
| 38 | + ...item, | ||
| 39 | + label: item.attribute, | ||
| 40 | + value: item.attribute, | ||
| 41 | + })); | ||
| 42 | + updateSchema([ | ||
| 43 | + { | ||
| 44 | + field: SchemaFiled.DEVICE_ID, | ||
| 45 | + componentProps: { | ||
| 46 | + options, | ||
| 47 | + }, | ||
| 48 | + }, | ||
| 49 | + { | ||
| 50 | + field: SchemaFiled.KEYS, | ||
| 51 | + component: 'Select', | ||
| 52 | + defaultValue: attKey.map((item) => item.value), | ||
| 53 | + componentProps: { | ||
| 54 | + options: attKey, | ||
| 55 | + mode: 'multiple', | ||
| 56 | + disabled: true, | ||
| 57 | + }, | ||
| 58 | + }, | ||
| 59 | + ]); | ||
| 60 | + | ||
| 61 | + setFieldsValue({ | ||
| 62 | + [SchemaFiled.DEVICE_ID]: deviceRecord.deviceId, | ||
| 63 | + [SchemaFiled.KEYS]: attKey.map((item) => item.value), | ||
| 64 | + }); | ||
| 65 | + } catch (error) { | ||
| 66 | + throw error; | ||
| 67 | + } | ||
| 68 | + }); | ||
| 69 | + | ||
| 70 | + const validEffective = (value = '') => { | ||
| 71 | + return !!(value && !isNaN(value as unknown as number)); | ||
| 72 | + }; | ||
| 73 | + const loading = ref(false); | ||
| 74 | + const handleOk = async () => { | ||
| 75 | + try { | ||
| 76 | + await validate(); | ||
| 77 | + let value = getFieldsValue(); | ||
| 78 | + | ||
| 79 | + value = getHistorySearchParams(value); | ||
| 80 | + | ||
| 81 | + loading.value = true; | ||
| 82 | + | ||
| 83 | + const res = await getDeviceHistoryInfo({ | ||
| 84 | + ...value, | ||
| 85 | + [SchemaFiled.KEYS]: value[SchemaFiled.KEYS].join(','), | ||
| 86 | + }); | ||
| 87 | + | ||
| 88 | + let timespanList = Object.keys(res).reduce((prev, next) => { | ||
| 89 | + const ts = res[next].map((item) => item.ts); | ||
| 90 | + return [...prev, ...ts]; | ||
| 91 | + }, [] as number[]); | ||
| 92 | + timespanList = [...new Set(timespanList)]; | ||
| 93 | + | ||
| 94 | + const track: Record<'lng' | 'lat', number>[] = []; | ||
| 95 | + const keys = Object.keys(res); | ||
| 96 | + | ||
| 97 | + for (const ts of timespanList) { | ||
| 98 | + const list: { ts: number; value: number }[] = []; | ||
| 99 | + for (const key of keys) { | ||
| 100 | + const record = res[key].find((item) => ts === item.ts); | ||
| 101 | + if (!validEffective(record?.value)) { | ||
| 102 | + continue; | ||
| 103 | + } | ||
| 104 | + list.push(record as any); | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + if (list.length === 2 && list.every(Boolean)) { | ||
| 108 | + const lng = list.at(0)?.value; | ||
| 109 | + const lat = list.at(1)?.value; | ||
| 110 | + if (lng && lat) track.push({ lng, lat }); | ||
| 111 | + } | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + emit('ok', { track, value } as HistoryModalOkEmitParams); | ||
| 115 | + closeModal(); | ||
| 116 | + } catch (error) { | ||
| 117 | + throw error; | ||
| 118 | + } finally { | ||
| 119 | + loading.value = false; | ||
| 120 | + } | ||
| 121 | + }; | ||
| 122 | +</script> | ||
| 123 | + | ||
| 124 | +<template> | ||
| 125 | + <BasicModal | ||
| 126 | + title="历史轨迹" | ||
| 127 | + @register="registerModal" | ||
| 128 | + @ok="handleOk" | ||
| 129 | + :ok-button-props="{ loading }" | ||
| 130 | + > | ||
| 131 | + <BasicForm @register="registerForm" /> | ||
| 132 | + </BasicModal> | ||
| 133 | +</template> |
| 1 | +import cloneDeep from 'lodash-es/cloneDeep'; | ||
| 2 | +import { MapComponentTrackHistoryConfig } from '.'; | ||
| 3 | +import { | ||
| 4 | + ConfigType, | ||
| 5 | + CreateComponentType, | ||
| 6 | + PublicComponentOptions, | ||
| 7 | + PublicPresetOptions, | ||
| 8 | +} from '/@/views/visual/packages/index.type'; | ||
| 9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | ||
| 10 | + | ||
| 11 | +export const option: PublicPresetOptions = { | ||
| 12 | + componetDesign: false, | ||
| 13 | + multipleDataSourceComponent: true, | ||
| 14 | +}; | ||
| 15 | + | ||
| 16 | +export default class Config extends PublicConfigClass implements CreateComponentType { | ||
| 17 | + public key: string = MapComponentTrackHistoryConfig.key; | ||
| 18 | + | ||
| 19 | + public attr = { ...componentInitConfig }; | ||
| 20 | + | ||
| 21 | + public componentConfig: ConfigType = cloneDeep(MapComponentTrackHistoryConfig); | ||
| 22 | + | ||
| 23 | + public persetOption = cloneDeep(option); | ||
| 24 | + | ||
| 25 | + public option: PublicComponentOptions; | ||
| 26 | + | ||
| 27 | + constructor(option: PublicComponentOptions) { | ||
| 28 | + super(); | ||
| 29 | + this.option = { ...option }; | ||
| 30 | + } | ||
| 31 | +} |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 3 | + import { useForm, BasicForm } from '/@/components/Form'; | ||
| 4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 5 | + | ||
| 6 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | ||
| 7 | + schemas: [ | ||
| 8 | + { | ||
| 9 | + field: ComponentConfigFieldEnum.FONT_COLOR, | ||
| 10 | + label: '数值字体颜色', | ||
| 11 | + component: 'ColorPicker', | ||
| 12 | + changeEvent: 'update:value', | ||
| 13 | + componentProps: { | ||
| 14 | + defaultValue: '#FD7347', | ||
| 15 | + }, | ||
| 16 | + }, | ||
| 17 | + { | ||
| 18 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | ||
| 19 | + label: '显示设备名称', | ||
| 20 | + component: 'Checkbox', | ||
| 21 | + }, | ||
| 22 | + ], | ||
| 23 | + showActionButtonGroup: false, | ||
| 24 | + labelWidth: 120, | ||
| 25 | + baseColProps: { | ||
| 26 | + span: 12, | ||
| 27 | + }, | ||
| 28 | + }); | ||
| 29 | + | ||
| 30 | + const getFormValues = () => { | ||
| 31 | + return getFieldsValue(); | ||
| 32 | + }; | ||
| 33 | + | ||
| 34 | + const setFormValues = (data: Recordable) => { | ||
| 35 | + return setFieldsValue(data); | ||
| 36 | + }; | ||
| 37 | + | ||
| 38 | + defineExpose({ | ||
| 39 | + getFormValues, | ||
| 40 | + setFormValues, | ||
| 41 | + resetFormValues: resetFields, | ||
| 42 | + } as PublicFormInstaceType); | ||
| 43 | +</script> | ||
| 44 | + | ||
| 45 | +<template> | ||
| 46 | + <BasicForm @register="register" /> | ||
| 47 | +</template> |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { CreateComponentType } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
| 4 | + import { | ||
| 5 | + PublicComponentValueType, | ||
| 6 | + PublicFormInstaceType, | ||
| 7 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 8 | + import { commonDataSourceSchemas } from '../../../config/common.config'; | ||
| 9 | + | ||
| 10 | + defineProps<{ | ||
| 11 | + values: PublicComponentValueType; | ||
| 12 | + componentConfig: CreateComponentType; | ||
| 13 | + }>(); | ||
| 14 | + | ||
| 15 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | ||
| 16 | + labelWidth: 0, | ||
| 17 | + showActionButtonGroup: false, | ||
| 18 | + layout: 'horizontal', | ||
| 19 | + labelCol: { span: 0 }, | ||
| 20 | + schemas: commonDataSourceSchemas(), | ||
| 21 | + }); | ||
| 22 | + | ||
| 23 | + const getFormValues = () => { | ||
| 24 | + return getFieldsValue(); | ||
| 25 | + }; | ||
| 26 | + | ||
| 27 | + const setFormValues = (record: Recordable) => { | ||
| 28 | + return setFieldsValue(record); | ||
| 29 | + }; | ||
| 30 | + | ||
| 31 | + defineExpose({ | ||
| 32 | + getFormValues, | ||
| 33 | + setFormValues, | ||
| 34 | + validate, | ||
| 35 | + resetFormValues: resetFields, | ||
| 36 | + } as PublicFormInstaceType); | ||
| 37 | +</script> | ||
| 38 | + | ||
| 39 | +<template> | ||
| 40 | + <BasicForm @register="register" /> | ||
| 41 | +</template> |
| 1 | +import moment from 'moment'; | ||
| 2 | +import { Moment } from 'moment'; | ||
| 3 | +import { FormSchema } from '/@/components/Form'; | ||
| 4 | +import { ColEx } from '/@/components/Form/src/types'; | ||
| 5 | +import { useGridLayout } from '/@/hooks/component/useGridLayout'; | ||
| 6 | +import { | ||
| 7 | + getPacketIntervalByRange, | ||
| 8 | + getPacketIntervalByValue, | ||
| 9 | + intervalOption, | ||
| 10 | +} from '/@/views/device/localtion/cpns/TimePeriodForm/helper'; | ||
| 11 | +export enum QueryWay { | ||
| 12 | + LATEST = 'latest', | ||
| 13 | + TIME_PERIOD = 'timePeriod', | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +export enum SchemaFiled { | ||
| 17 | + DEVICE_ID = 'deviceId', | ||
| 18 | + WAY = 'way', | ||
| 19 | + TIME_PERIOD = 'timePeriod', | ||
| 20 | + KEYS = 'keys', | ||
| 21 | + DATE_RANGE = 'dataRange', | ||
| 22 | + START_TS = 'startTs', | ||
| 23 | + END_TS = 'endTs', | ||
| 24 | + INTERVAL = 'interval', | ||
| 25 | + LIMIT = 'limit', | ||
| 26 | + AGG = 'agg', | ||
| 27 | + ORDER_BY = 'orderBy', | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +export enum AggregateDataEnum { | ||
| 31 | + MIN = 'MIN', | ||
| 32 | + MAX = 'MAX', | ||
| 33 | + AVG = 'AVG', | ||
| 34 | + SUM = 'SUM', | ||
| 35 | + COUNT = 'COUNT', | ||
| 36 | + NONE = 'NONE', | ||
| 37 | +} | ||
| 38 | +export const formSchema = (): FormSchema[] => { | ||
| 39 | + return [ | ||
| 40 | + { | ||
| 41 | + field: SchemaFiled.DEVICE_ID, | ||
| 42 | + label: '设备名称', | ||
| 43 | + component: 'Select', | ||
| 44 | + rules: [{ required: true, message: '设备名称为必选项', type: 'string' }], | ||
| 45 | + componentProps({ formActionType }) { | ||
| 46 | + const { setFieldsValue } = formActionType; | ||
| 47 | + return { | ||
| 48 | + placeholder: '请选择设备', | ||
| 49 | + onChange() { | ||
| 50 | + setFieldsValue({ [SchemaFiled.KEYS]: null }); | ||
| 51 | + }, | ||
| 52 | + }; | ||
| 53 | + }, | ||
| 54 | + }, | ||
| 55 | + { | ||
| 56 | + field: SchemaFiled.WAY, | ||
| 57 | + label: '查询方式', | ||
| 58 | + component: 'RadioGroup', | ||
| 59 | + defaultValue: QueryWay.LATEST, | ||
| 60 | + componentProps({ formActionType }) { | ||
| 61 | + const { setFieldsValue } = formActionType; | ||
| 62 | + return { | ||
| 63 | + options: [ | ||
| 64 | + { label: '最后', value: QueryWay.LATEST }, | ||
| 65 | + { label: '时间段', value: QueryWay.TIME_PERIOD }, | ||
| 66 | + ], | ||
| 67 | + onChange(event: ChangeEvent) { | ||
| 68 | + (event.target as HTMLInputElement).value === QueryWay.LATEST | ||
| 69 | + ? setFieldsValue({ | ||
| 70 | + [SchemaFiled.DATE_RANGE]: [], | ||
| 71 | + [SchemaFiled.START_TS]: null, | ||
| 72 | + [SchemaFiled.END_TS]: null, | ||
| 73 | + }) | ||
| 74 | + : setFieldsValue({ [SchemaFiled.START_TS]: null }); | ||
| 75 | + }, | ||
| 76 | + getPopupContainer: () => document.body, | ||
| 77 | + }; | ||
| 78 | + }, | ||
| 79 | + }, | ||
| 80 | + { | ||
| 81 | + field: SchemaFiled.START_TS, | ||
| 82 | + label: '最后数据', | ||
| 83 | + component: 'Select', | ||
| 84 | + ifShow({ values }) { | ||
| 85 | + return values[SchemaFiled.WAY] === QueryWay.LATEST; | ||
| 86 | + }, | ||
| 87 | + componentProps({ formActionType }) { | ||
| 88 | + const { setFieldsValue } = formActionType; | ||
| 89 | + return { | ||
| 90 | + options: intervalOption, | ||
| 91 | + onChange() { | ||
| 92 | + setFieldsValue({ [SchemaFiled.INTERVAL]: null }); | ||
| 93 | + }, | ||
| 94 | + getPopupContainer: () => document.body, | ||
| 95 | + }; | ||
| 96 | + }, | ||
| 97 | + rules: [{ required: true, message: '最后数据为必选项', type: 'number' }], | ||
| 98 | + }, | ||
| 99 | + { | ||
| 100 | + field: SchemaFiled.DATE_RANGE, | ||
| 101 | + label: '时间段', | ||
| 102 | + component: 'RangePicker', | ||
| 103 | + ifShow({ values }) { | ||
| 104 | + return values[SchemaFiled.WAY] === QueryWay.TIME_PERIOD; | ||
| 105 | + }, | ||
| 106 | + rules: [{ required: true, message: '时间段为必选项' }], | ||
| 107 | + componentProps({ formActionType }) { | ||
| 108 | + const { setFieldsValue } = formActionType; | ||
| 109 | + let dates: Moment[] = []; | ||
| 110 | + return { | ||
| 111 | + showTime: { | ||
| 112 | + defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')], | ||
| 113 | + }, | ||
| 114 | + onCalendarChange(value: Moment[]) { | ||
| 115 | + dates = value; | ||
| 116 | + }, | ||
| 117 | + disabledDate(current: Moment) { | ||
| 118 | + if (!dates || dates.length === 0 || !current) { | ||
| 119 | + return false; | ||
| 120 | + } | ||
| 121 | + const diffDate = current.diff(dates[0], 'years', true); | ||
| 122 | + return Math.abs(diffDate) > 1; | ||
| 123 | + }, | ||
| 124 | + onChange() { | ||
| 125 | + dates = []; | ||
| 126 | + setFieldsValue({ [SchemaFiled.INTERVAL]: null }); | ||
| 127 | + }, | ||
| 128 | + getPopupContainer: () => document.body, | ||
| 129 | + }; | ||
| 130 | + }, | ||
| 131 | + colProps: useGridLayout(2, 2, 2, 2, 2, 2) as unknown as ColEx, | ||
| 132 | + }, | ||
| 133 | + { | ||
| 134 | + field: SchemaFiled.AGG, | ||
| 135 | + label: '数据聚合功能', | ||
| 136 | + component: 'Select', | ||
| 137 | + componentProps: { | ||
| 138 | + getPopupContainer: () => document.body, | ||
| 139 | + options: [ | ||
| 140 | + { label: '最小值', value: AggregateDataEnum.MIN }, | ||
| 141 | + { label: '最大值', value: AggregateDataEnum.MAX }, | ||
| 142 | + { label: '平均值', value: AggregateDataEnum.AVG }, | ||
| 143 | + { label: '求和', value: AggregateDataEnum.SUM }, | ||
| 144 | + { label: '计数', value: AggregateDataEnum.COUNT }, | ||
| 145 | + { label: '空', value: AggregateDataEnum.NONE }, | ||
| 146 | + ], | ||
| 147 | + }, | ||
| 148 | + }, | ||
| 149 | + { | ||
| 150 | + field: SchemaFiled.INTERVAL, | ||
| 151 | + label: '分组间隔', | ||
| 152 | + component: 'Select', | ||
| 153 | + dynamicRules: ({ model }) => { | ||
| 154 | + return [ | ||
| 155 | + { | ||
| 156 | + required: model[SchemaFiled.AGG] !== AggregateDataEnum.NONE, | ||
| 157 | + message: '分组间隔为必填项', | ||
| 158 | + type: 'number', | ||
| 159 | + }, | ||
| 160 | + ]; | ||
| 161 | + }, | ||
| 162 | + ifShow({ values }) { | ||
| 163 | + return values[SchemaFiled.AGG] !== AggregateDataEnum.NONE; | ||
| 164 | + }, | ||
| 165 | + componentProps({ formModel, formActionType }) { | ||
| 166 | + const options = | ||
| 167 | + formModel[SchemaFiled.WAY] === QueryWay.LATEST | ||
| 168 | + ? getPacketIntervalByValue(formModel[SchemaFiled.START_TS]) | ||
| 169 | + : getPacketIntervalByRange(formModel[SchemaFiled.DATE_RANGE]); | ||
| 170 | + if (formModel[SchemaFiled.AGG] !== AggregateDataEnum.NONE) { | ||
| 171 | + formActionType.setFieldsValue({ [SchemaFiled.LIMIT]: null }); | ||
| 172 | + } | ||
| 173 | + return { | ||
| 174 | + options, | ||
| 175 | + getPopupContainer: () => document.body, | ||
| 176 | + }; | ||
| 177 | + }, | ||
| 178 | + }, | ||
| 179 | + { | ||
| 180 | + field: SchemaFiled.LIMIT, | ||
| 181 | + label: '最大条数', | ||
| 182 | + component: 'InputNumber', | ||
| 183 | + ifShow({ values }) { | ||
| 184 | + return values[SchemaFiled.AGG] === AggregateDataEnum.NONE; | ||
| 185 | + }, | ||
| 186 | + rules: [{ required: true, message: '最大条数为必填项' }], | ||
| 187 | + helpMessage: ['根据查询条件,查出的数据条数不超过这个值'], | ||
| 188 | + componentProps() { | ||
| 189 | + return { | ||
| 190 | + max: 50000, | ||
| 191 | + min: 7, | ||
| 192 | + getPopupContainer: () => document.body, | ||
| 193 | + }; | ||
| 194 | + }, | ||
| 195 | + }, | ||
| 196 | + { | ||
| 197 | + field: SchemaFiled.KEYS, | ||
| 198 | + label: '设备属性', | ||
| 199 | + component: 'Select', | ||
| 200 | + componentProps: { | ||
| 201 | + getPopupContainer: () => document.body, | ||
| 202 | + }, | ||
| 203 | + }, | ||
| 204 | + ]; | ||
| 205 | +}; | ||
| 206 | + | ||
| 207 | +export function getHistorySearchParams(value: Recordable) { | ||
| 208 | + const { startTs, endTs, interval, agg, limit, way, keys, deviceId } = value; | ||
| 209 | + if (way === QueryWay.LATEST) { | ||
| 210 | + return { | ||
| 211 | + keys, | ||
| 212 | + entityId: deviceId, | ||
| 213 | + startTs: moment().subtract(startTs, 'ms').valueOf(), | ||
| 214 | + endTs: Date.now(), | ||
| 215 | + interval, | ||
| 216 | + agg, | ||
| 217 | + limit, | ||
| 218 | + }; | ||
| 219 | + } else { | ||
| 220 | + return { | ||
| 221 | + keys, | ||
| 222 | + entityId: deviceId, | ||
| 223 | + startTs: moment(startTs).valueOf(), | ||
| 224 | + endTs: moment(endTs).valueOf(), | ||
| 225 | + interval, | ||
| 226 | + agg, | ||
| 227 | + limit, | ||
| 228 | + }; | ||
| 229 | + } | ||
| 230 | +} |
| 1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | ||
| 2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | ||
| 3 | + | ||
| 4 | +const componentKeys = useComponentKeys('MapComponentTrackHistory'); | ||
| 5 | + | ||
| 6 | +export const MapComponentTrackHistoryConfig: ConfigType = { | ||
| 7 | + ...componentKeys, | ||
| 8 | + title: '历史轨迹', | ||
| 9 | + package: PackagesCategoryEnum.MAP, | ||
| 10 | +}; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentPropsConfigType } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { option } from './config'; | ||
| 4 | + import HistoryDataModel from './HistoryDataModal.vue'; | ||
| 5 | + import { useModal } from '/@/components/Modal'; | ||
| 6 | + import { Button, Tooltip, Spin } from 'ant-design-vue'; | ||
| 7 | + import { | ||
| 8 | + PlayCircleOutlined, | ||
| 9 | + PauseCircleOutlined, | ||
| 10 | + ClockCircleOutlined, | ||
| 11 | + } from '@ant-design/icons-vue'; | ||
| 12 | + import { computed, ref, toRaw, unref } from 'vue'; | ||
| 13 | + import { buildUUID } from '/@/utils/uuid'; | ||
| 14 | + import { useBaiduMapSDK } from '../../../hook/useBaiduMapSDK'; | ||
| 15 | + import { HistoryModalOkEmitParams, TrackAnimationStatus } from './type'; | ||
| 16 | + import { useMapTrackPlayback } from './useMapTrackPlayback'; | ||
| 17 | + import { shallowRef } from 'vue'; | ||
| 18 | + import { formatToDateTime } from '/@/utils/dateUtil'; | ||
| 19 | + | ||
| 20 | + const props = defineProps<{ | ||
| 21 | + config: ComponentPropsConfigType<typeof option>; | ||
| 22 | + }>(); | ||
| 23 | + | ||
| 24 | + const wrapRef = ref(); | ||
| 25 | + | ||
| 26 | + const wrapId = `bai-map-${buildUUID()}`; | ||
| 27 | + | ||
| 28 | + const mapInstance = shallowRef<Nullable<Recordable>>(null); | ||
| 29 | + | ||
| 30 | + const rangString = ref<Nullable<string>>(null); | ||
| 31 | + | ||
| 32 | + const [register, { openModal }] = useModal(); | ||
| 33 | + | ||
| 34 | + const handleTrackSwitch = () => { | ||
| 35 | + openModal(true, toRaw(props.config.option.dataSource)); | ||
| 36 | + }; | ||
| 37 | + | ||
| 38 | + const getIsWidgetLibSelectMode = computed(() => { | ||
| 39 | + return !props.config.option.dataSource; | ||
| 40 | + }); | ||
| 41 | + | ||
| 42 | + const handleRenderHistroyData = (data: HistoryModalOkEmitParams) => { | ||
| 43 | + const { track, value } = data; | ||
| 44 | + const { startTs, endTs } = value; | ||
| 45 | + const formatType = 'YYYY-MM-DD HH:mm:ss'; | ||
| 46 | + rangString.value = `从${formatToDateTime(startTs, formatType)} 到 ${formatToDateTime( | ||
| 47 | + endTs, | ||
| 48 | + formatType | ||
| 49 | + )}`; | ||
| 50 | + genTrackPlaybackAnimation(track as any[]); | ||
| 51 | + }; | ||
| 52 | + | ||
| 53 | + const handlePlay = () => { | ||
| 54 | + if (unref(playStatus) === TrackAnimationStatus.PAUSE) { | ||
| 55 | + unref(continueFn)?.(); | ||
| 56 | + return; | ||
| 57 | + } | ||
| 58 | + if (unref(playStatus) === TrackAnimationStatus.DONE) { | ||
| 59 | + unref(playFn)?.(); | ||
| 60 | + return; | ||
| 61 | + } | ||
| 62 | + if (unref(playStatus) === TrackAnimationStatus.PLAY) { | ||
| 63 | + unref(pauseFn)?.(); | ||
| 64 | + return; | ||
| 65 | + } | ||
| 66 | + }; | ||
| 67 | + | ||
| 68 | + async function initMap() { | ||
| 69 | + const wrapEl = unref(wrapRef); | ||
| 70 | + if (!wrapEl) return; | ||
| 71 | + const BMapGL = (window as any).BMapGL; | ||
| 72 | + if (!Reflect.has(window, 'BMapGL')) return; | ||
| 73 | + mapInstance.value = new BMapGL.Map(wrapId); | ||
| 74 | + | ||
| 75 | + // 定位当前城市 | ||
| 76 | + const localcity = new BMapGL.LocalCity(); | ||
| 77 | + localcity.get( | ||
| 78 | + (e: { center: Record<'lat' | 'lng', number>; code: number; level: number; name: string }) => { | ||
| 79 | + const { center } = e; | ||
| 80 | + const { lat, lng } = center; | ||
| 81 | + const point = new BMapGL.Point(lng, lat); | ||
| 82 | + unref(mapInstance)!.centerAndZoom(point, 15); | ||
| 83 | + } | ||
| 84 | + ); | ||
| 85 | + | ||
| 86 | + unref(mapInstance)!.enableScrollWheelZoom(true); | ||
| 87 | + | ||
| 88 | + unref(getIsWidgetLibSelectMode) && genTrackPlaybackAnimation(); | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + const { loading } = useBaiduMapSDK(initMap); | ||
| 92 | + | ||
| 93 | + const { genTrackPlaybackAnimation, playStatus, playFn, continueFn, pauseFn } = | ||
| 94 | + useMapTrackPlayback(mapInstance); | ||
| 95 | +</script> | ||
| 96 | + | ||
| 97 | +<template> | ||
| 98 | + <main class="w-full h-full flex flex-col justify-center items-center"> | ||
| 99 | + <div class="w-full flex justify-end"> | ||
| 100 | + <Button | ||
| 101 | + type="text" | ||
| 102 | + class="!px-2 flex-auto !text-left truncate mr-2" | ||
| 103 | + :disabled="getIsWidgetLibSelectMode" | ||
| 104 | + @click="handleTrackSwitch" | ||
| 105 | + > | ||
| 106 | + <div class="w-full truncate text-gray-500 flex items-center"> | ||
| 107 | + <ClockCircleOutlined class="mx-1" /> | ||
| 108 | + <span>历史</span> | ||
| 109 | + <span class="mx-1">-</span> | ||
| 110 | + <Tooltip :title="rangString || '请选择'"> | ||
| 111 | + <span class="truncate"> {{ rangString || '请选择' }} </span> | ||
| 112 | + </Tooltip> | ||
| 113 | + </div> | ||
| 114 | + </Button> | ||
| 115 | + <Button type="text" class="!px-2 !text-gray-500" @click="handlePlay"> | ||
| 116 | + <PlayCircleOutlined v-show="playStatus !== TrackAnimationStatus.PLAY" /> | ||
| 117 | + <PauseCircleOutlined class="!ml-0" v-show="playStatus === TrackAnimationStatus.PLAY" /> | ||
| 118 | + <span> | ||
| 119 | + {{ playStatus !== TrackAnimationStatus.PLAY ? '播放轨迹' : '暂停播放' }} | ||
| 120 | + </span> | ||
| 121 | + </Button> | ||
| 122 | + </div> | ||
| 123 | + <Spin | ||
| 124 | + :spinning="loading" | ||
| 125 | + wrapper-class-name="map-spin-wrapper !w-full !h-full !flex justify-center items-center pointer-events-none" | ||
| 126 | + tip="地图加载中..." | ||
| 127 | + > | ||
| 128 | + <div ref="wrapRef" :id="wrapId" class="w-full h-full no-drag"> </div> | ||
| 129 | + </Spin> | ||
| 130 | + | ||
| 131 | + <HistoryDataModel @register="register" @ok="handleRenderHistroyData" /> | ||
| 132 | + </main> | ||
| 133 | +</template> | ||
| 134 | + | ||
| 135 | +<style lang="less" scoped> | ||
| 136 | + .map-spin-wrapper { | ||
| 137 | + :deep(.ant-spin-container) { | ||
| 138 | + @apply justify-center items-center p-2 w-full h-full; | ||
| 139 | + } | ||
| 140 | + } | ||
| 141 | +</style> |
| 1 | +import { SchemaFiled } from './history.config'; | ||
| 2 | +import { DataSource } from '/@/views/visual/palette/types'; | ||
| 3 | + | ||
| 4 | +export interface HistoryModalParams { | ||
| 5 | + dataSource?: DataSource[]; | ||
| 6 | +} | ||
| 7 | + | ||
| 8 | +export interface HistoryModalOkEmitParams { | ||
| 9 | + track: { | ||
| 10 | + lng: number | string; | ||
| 11 | + lat: number | string; | ||
| 12 | + }[]; | ||
| 13 | + value: Record<SchemaFiled, string | number>; | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +export enum TrackAnimationStatus { | ||
| 17 | + /** | ||
| 18 | + * @description 播放中的状态 | ||
| 19 | + */ | ||
| 20 | + PLAY = 1, | ||
| 21 | + | ||
| 22 | + /** | ||
| 23 | + * @description 完成和未开始的状态 | ||
| 24 | + */ | ||
| 25 | + DONE = 2, | ||
| 26 | + | ||
| 27 | + /** | ||
| 28 | + * @description 暂停时的状态 | ||
| 29 | + */ | ||
| 30 | + PAUSE = 3, | ||
| 31 | +} |
| 1 | +import { ShallowRef, computed, ref, shallowRef, unref } from 'vue'; | ||
| 2 | +import { TrackAnimationStatus } from './type'; | ||
| 3 | +import { useTimeoutFn } from '@vueuse/core'; | ||
| 4 | +import { useMessage } from '/@/hooks/web/useMessage'; | ||
| 5 | + | ||
| 6 | +export const useMapTrackPlayback = (mapInstance: ShallowRef<Nullable<Recordable>>) => { | ||
| 7 | + const { createMessage } = useMessage(); | ||
| 8 | + | ||
| 9 | + const trackAni = ref<Nullable<Recordable>>(null); | ||
| 10 | + | ||
| 11 | + const playFn = shallowRef<Nullable<Fn>>(() => { | ||
| 12 | + trackAni.value?.start?.(); | ||
| 13 | + }); | ||
| 14 | + | ||
| 15 | + const pauseFn = shallowRef<Nullable<Fn>>(() => { | ||
| 16 | + trackAni.value?.pause?.(); | ||
| 17 | + }); | ||
| 18 | + | ||
| 19 | + const continueFn = shallowRef<Nullable<Fn>>(() => { | ||
| 20 | + trackAni.value?.continue?.(); | ||
| 21 | + }); | ||
| 22 | + | ||
| 23 | + const playStatus = computed<TrackAnimationStatus>(() => { | ||
| 24 | + return unref(trackAni)?._status; | ||
| 25 | + }); | ||
| 26 | + | ||
| 27 | + const RANDOM_PATH = [ | ||
| 28 | + { | ||
| 29 | + lng: 116.297611, | ||
| 30 | + lat: 40.047363, | ||
| 31 | + }, | ||
| 32 | + { | ||
| 33 | + lng: 116.302839, | ||
| 34 | + lat: 40.048219, | ||
| 35 | + }, | ||
| 36 | + { | ||
| 37 | + lng: 116.308301, | ||
| 38 | + lat: 40.050566, | ||
| 39 | + }, | ||
| 40 | + { | ||
| 41 | + lng: 116.305732, | ||
| 42 | + lat: 40.054957, | ||
| 43 | + }, | ||
| 44 | + { | ||
| 45 | + lng: 116.304754, | ||
| 46 | + lat: 40.057953, | ||
| 47 | + }, | ||
| 48 | + { | ||
| 49 | + lng: 116.306487, | ||
| 50 | + lat: 40.058312, | ||
| 51 | + }, | ||
| 52 | + { | ||
| 53 | + lng: 116.307223, | ||
| 54 | + lat: 40.056379, | ||
| 55 | + }, | ||
| 56 | + ]; | ||
| 57 | + | ||
| 58 | + const getAnimationDurationTime = (length: number) => { | ||
| 59 | + const itemDurationTime = 1000; | ||
| 60 | + const minDurationTime = 5000; | ||
| 61 | + const durationTime = length * itemDurationTime; | ||
| 62 | + return Math.max(durationTime, minDurationTime); | ||
| 63 | + }; | ||
| 64 | + | ||
| 65 | + /** | ||
| 66 | + * @demo https://lbsyun.baidu.com/jsdemo.htm#fAnimationPause | ||
| 67 | + * @param path | ||
| 68 | + * @param clearOverlays | ||
| 69 | + */ | ||
| 70 | + const genTrackPlaybackAnimation = ( | ||
| 71 | + path?: Record<'lng' | 'lat', number>[], | ||
| 72 | + clearOverlays = true | ||
| 73 | + ) => { | ||
| 74 | + path = path || RANDOM_PATH; | ||
| 75 | + | ||
| 76 | + if (!path.length) { | ||
| 77 | + createMessage.warning('无可用数据~'); | ||
| 78 | + return; | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + const point: any[] = []; | ||
| 82 | + const BMapGL = (window as any).BMapGL; | ||
| 83 | + | ||
| 84 | + clearOverlays && unref(mapInstance)?.clearOverlays(); | ||
| 85 | + | ||
| 86 | + for (const { lng, lat } of path) { | ||
| 87 | + point.push(new BMapGL.Point(lng, lat)); | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + const pl = new BMapGL.Polyline(point); | ||
| 91 | + const BMapGLLib = (window as any).BMapGLLib; | ||
| 92 | + | ||
| 93 | + for (const item of path) { | ||
| 94 | + marketPoint(item); | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + const duration = getAnimationDurationTime(path?.length || 0); | ||
| 98 | + const delay = 300; | ||
| 99 | + trackAni.value = new BMapGLLib.TrackAnimation(unref(mapInstance), pl, { | ||
| 100 | + overallView: true, | ||
| 101 | + tilt: 30, | ||
| 102 | + duration, | ||
| 103 | + delay, | ||
| 104 | + }); | ||
| 105 | + | ||
| 106 | + useTimeoutFn(() => { | ||
| 107 | + unref(playFn)?.(); | ||
| 108 | + }, 500); | ||
| 109 | + }; | ||
| 110 | + | ||
| 111 | + function marketPoint(params: Partial<Record<'lng' | 'lat', number>>) { | ||
| 112 | + const { lng, lat } = params; | ||
| 113 | + if (![lng, lat].every(Boolean)) return; | ||
| 114 | + const BMap = (window as any).BMapGL; | ||
| 115 | + const marker = new BMap.Marker(new BMap.Point(lng, lat)); | ||
| 116 | + unref(mapInstance)?.centerAndZoom(new BMap.Point(lng, lat)); | ||
| 117 | + unref(mapInstance)?.addOverlay(marker); | ||
| 118 | + } | ||
| 119 | + | ||
| 120 | + return { genTrackPlaybackAnimation, playFn, pauseFn, continueFn, playStatus }; | ||
| 121 | +}; |
| 1 | +import cloneDeep from 'lodash-es/cloneDeep'; | ||
| 2 | +import { MapComponentTrackRealConfig } from '.'; | ||
| 3 | +import { | ||
| 4 | + ConfigType, | ||
| 5 | + CreateComponentType, | ||
| 6 | + PublicComponentOptions, | ||
| 7 | + PublicPresetOptions, | ||
| 8 | +} from '/@/views/visual/packages/index.type'; | ||
| 9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | ||
| 10 | + | ||
| 11 | +export const option: PublicPresetOptions = { | ||
| 12 | + componetDesign: false, | ||
| 13 | + multipleDataSourceComponent: true, | ||
| 14 | +}; | ||
| 15 | + | ||
| 16 | +export default class Config extends PublicConfigClass implements CreateComponentType { | ||
| 17 | + public key: string = MapComponentTrackRealConfig.key; | ||
| 18 | + | ||
| 19 | + public attr = { ...componentInitConfig }; | ||
| 20 | + | ||
| 21 | + public componentConfig: ConfigType = cloneDeep(MapComponentTrackRealConfig); | ||
| 22 | + | ||
| 23 | + public persetOption = cloneDeep(option); | ||
| 24 | + | ||
| 25 | + public option: PublicComponentOptions; | ||
| 26 | + | ||
| 27 | + constructor(option: PublicComponentOptions) { | ||
| 28 | + super(); | ||
| 29 | + this.option = { ...option }; | ||
| 30 | + } | ||
| 31 | +} |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 3 | + import { useForm, BasicForm } from '/@/components/Form'; | ||
| 4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 5 | + | ||
| 6 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | ||
| 7 | + schemas: [ | ||
| 8 | + { | ||
| 9 | + field: ComponentConfigFieldEnum.FONT_COLOR, | ||
| 10 | + label: '数值字体颜色', | ||
| 11 | + component: 'ColorPicker', | ||
| 12 | + changeEvent: 'update:value', | ||
| 13 | + componentProps: { | ||
| 14 | + defaultValue: '#FD7347', | ||
| 15 | + }, | ||
| 16 | + }, | ||
| 17 | + { | ||
| 18 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | ||
| 19 | + label: '显示设备名称', | ||
| 20 | + component: 'Checkbox', | ||
| 21 | + }, | ||
| 22 | + ], | ||
| 23 | + showActionButtonGroup: false, | ||
| 24 | + labelWidth: 120, | ||
| 25 | + baseColProps: { | ||
| 26 | + span: 12, | ||
| 27 | + }, | ||
| 28 | + }); | ||
| 29 | + | ||
| 30 | + const getFormValues = () => { | ||
| 31 | + return getFieldsValue(); | ||
| 32 | + }; | ||
| 33 | + | ||
| 34 | + const setFormValues = (data: Recordable) => { | ||
| 35 | + return setFieldsValue(data); | ||
| 36 | + }; | ||
| 37 | + | ||
| 38 | + defineExpose({ | ||
| 39 | + getFormValues, | ||
| 40 | + setFormValues, | ||
| 41 | + resetFormValues: resetFields, | ||
| 42 | + } as PublicFormInstaceType); | ||
| 43 | +</script> | ||
| 44 | + | ||
| 45 | +<template> | ||
| 46 | + <BasicForm @register="register" /> | ||
| 47 | +</template> |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { CreateComponentType } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
| 4 | + import { | ||
| 5 | + PublicComponentValueType, | ||
| 6 | + PublicFormInstaceType, | ||
| 7 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 8 | + import { commonDataSourceSchemas } from '../../../config/common.config'; | ||
| 9 | + | ||
| 10 | + defineProps<{ | ||
| 11 | + values: PublicComponentValueType; | ||
| 12 | + componentConfig: CreateComponentType; | ||
| 13 | + }>(); | ||
| 14 | + | ||
| 15 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | ||
| 16 | + labelWidth: 0, | ||
| 17 | + showActionButtonGroup: false, | ||
| 18 | + layout: 'horizontal', | ||
| 19 | + labelCol: { span: 0 }, | ||
| 20 | + schemas: commonDataSourceSchemas(), | ||
| 21 | + }); | ||
| 22 | + | ||
| 23 | + const getFormValues = () => { | ||
| 24 | + return getFieldsValue(); | ||
| 25 | + }; | ||
| 26 | + | ||
| 27 | + const setFormValues = (record: Recordable) => { | ||
| 28 | + return setFieldsValue(record); | ||
| 29 | + }; | ||
| 30 | + | ||
| 31 | + defineExpose({ | ||
| 32 | + getFormValues, | ||
| 33 | + setFormValues, | ||
| 34 | + validate, | ||
| 35 | + resetFormValues: resetFields, | ||
| 36 | + } as PublicFormInstaceType); | ||
| 37 | +</script> | ||
| 38 | + | ||
| 39 | +<template> | ||
| 40 | + <BasicForm @register="register" /> | ||
| 41 | +</template> |
| 1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | ||
| 2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | ||
| 3 | + | ||
| 4 | +const componentKeys = useComponentKeys('MapComponentTrackReal'); | ||
| 5 | + | ||
| 6 | +export const MapComponentTrackRealConfig: ConfigType = { | ||
| 7 | + ...componentKeys, | ||
| 8 | + title: '实时轨迹', | ||
| 9 | + package: PackagesCategoryEnum.MAP, | ||
| 10 | +}; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { | ||
| 3 | + ComponentPropsConfigType, | ||
| 4 | + MultipleDataFetchUpdateFn, | ||
| 5 | + } from '/@/views/visual/packages/index.type'; | ||
| 6 | + import { option } from './config'; | ||
| 7 | + import { useMultipleDataFetch } from '/@/views/visual/packages/hook/useSocket'; | ||
| 8 | + import { useBaiduMapSDK } from '../../../hook/useBaiduMapSDK'; | ||
| 9 | + import { ref, unref } from 'vue'; | ||
| 10 | + import { buildUUID } from '/@/utils/uuid'; | ||
| 11 | + import { Spin } from 'ant-design-vue'; | ||
| 12 | + import { computed } from 'vue'; | ||
| 13 | + import { useMapTrackPlayBack } from './useMapTrackPlayback'; | ||
| 14 | + | ||
| 15 | + const props = defineProps<{ | ||
| 16 | + config: ComponentPropsConfigType<typeof option>; | ||
| 17 | + }>(); | ||
| 18 | + | ||
| 19 | + const wrapRef = ref<Nullable<Recordable>>(null); | ||
| 20 | + | ||
| 21 | + const mapInstance = ref<Nullable<Recordable>>(null); | ||
| 22 | + | ||
| 23 | + const wrapId = `bai-map-${buildUUID()}`; | ||
| 24 | + | ||
| 25 | + /** | ||
| 26 | + * @description 经度key | ||
| 27 | + */ | ||
| 28 | + const getLngKey = computed(() => { | ||
| 29 | + return props.config.option.dataSource?.at(0)?.attribute || ''; | ||
| 30 | + }); | ||
| 31 | + | ||
| 32 | + /** | ||
| 33 | + * @description 纬度key | ||
| 34 | + */ | ||
| 35 | + const getLatKey = computed(() => { | ||
| 36 | + return props.config.option.dataSource?.at(1)?.attribute || ''; | ||
| 37 | + }); | ||
| 38 | + | ||
| 39 | + const validEffective = (value = '') => { | ||
| 40 | + return !!(value && !isNaN(value as unknown as number)); | ||
| 41 | + }; | ||
| 42 | + | ||
| 43 | + const updateFn: MultipleDataFetchUpdateFn = (message) => { | ||
| 44 | + const { data = {} } = message; | ||
| 45 | + | ||
| 46 | + const lngData = data[unref(getLngKey)] || []; | ||
| 47 | + const [lngLatest] = lngData; | ||
| 48 | + const [, lng] = lngLatest; | ||
| 49 | + | ||
| 50 | + const latData = data[unref(getLatKey)] || []; | ||
| 51 | + const [latLatest] = latData; | ||
| 52 | + const [, lat] = latLatest; | ||
| 53 | + | ||
| 54 | + if (validEffective(lng) && validEffective(lat)) { | ||
| 55 | + drawLine({ lng: Number(lng), lat: Number(lat) }); | ||
| 56 | + } | ||
| 57 | + }; | ||
| 58 | + | ||
| 59 | + useMultipleDataFetch(props, updateFn); | ||
| 60 | + | ||
| 61 | + const { drawLine } = useMapTrackPlayBack(mapInstance); | ||
| 62 | + | ||
| 63 | + const initMap = () => { | ||
| 64 | + const wrapEl = unref(wrapRef); | ||
| 65 | + if (!wrapEl) return; | ||
| 66 | + if (!Reflect.has(window, 'BMapGL')) return; | ||
| 67 | + const BMapGL = (window as any).BMapGL; | ||
| 68 | + mapInstance.value = new BMapGL.Map(wrapId); | ||
| 69 | + | ||
| 70 | + // 定位当前城市 | ||
| 71 | + const localcity = new BMapGL.LocalCity(); | ||
| 72 | + localcity.get( | ||
| 73 | + (e: { center: Record<'lat' | 'lng', number>; code: number; level: number; name: string }) => { | ||
| 74 | + const { center } = e; | ||
| 75 | + const { lat, lng } = center; | ||
| 76 | + const point = new BMapGL.Point(lng, lat); | ||
| 77 | + unref(mapInstance)!.centerAndZoom(point, 15); | ||
| 78 | + } | ||
| 79 | + ); | ||
| 80 | + | ||
| 81 | + unref(mapInstance)!.enableScrollWheelZoom(true); | ||
| 82 | + }; | ||
| 83 | + | ||
| 84 | + const { loading } = useBaiduMapSDK(initMap); | ||
| 85 | +</script> | ||
| 86 | + | ||
| 87 | +<template> | ||
| 88 | + <main class="w-full h-full flex flex-col p-2 justify-center items-center"> | ||
| 89 | + <Spin | ||
| 90 | + :spinning="loading" | ||
| 91 | + wrapper-class-name="map-spin-wrapper !w-full !h-full !flex justify-center items-center pointer-events-none" | ||
| 92 | + tip="地图加载中..." | ||
| 93 | + /> | ||
| 94 | + <div v-show="!loading" ref="wrapRef" :id="wrapId" class="w-full h-full no-drag"> </div> | ||
| 95 | + </main> | ||
| 96 | +</template> | ||
| 97 | + | ||
| 98 | +<style lang="less" scoped> | ||
| 99 | + // .map-spin-wrapper { | ||
| 100 | + // :deep(.ant-spin-container) { | ||
| 101 | + // @apply justify-center items-center p-2 w-full h-full; | ||
| 102 | + // } | ||
| 103 | + // } | ||
| 104 | +</style> |
| 1 | +import { Ref, unref } from 'vue'; | ||
| 2 | + | ||
| 3 | +export const useMapTrackPlayBack = (mapInstance: Ref<Nullable<Recordable>>) => { | ||
| 4 | + const positionList: Record<'lng' | 'lat', number>[] = []; | ||
| 5 | + | ||
| 6 | + const drawLine = (position: Record<'lng' | 'lat', number>) => { | ||
| 7 | + try { | ||
| 8 | + if (!position.lat || !position.lng) return; | ||
| 9 | + positionList.push(position); | ||
| 10 | + marketPoint(position); | ||
| 11 | + if (positionList.length === 1) return; | ||
| 12 | + | ||
| 13 | + const point: any[] = []; | ||
| 14 | + const BMapGL = (window as any).BMapGL; | ||
| 15 | + | ||
| 16 | + for (const { lng, lat } of positionList.slice(-2)) { | ||
| 17 | + point.push(new BMapGL.Point(lng, lat)); | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + const pl = new BMapGL.Polyline(point); | ||
| 21 | + const BMapGLLib = (window as any).BMapGLLib; | ||
| 22 | + | ||
| 23 | + const duration = 2000; | ||
| 24 | + const delay = 300; | ||
| 25 | + const trackAni = new BMapGLLib.TrackAnimation(unref(mapInstance), pl, { | ||
| 26 | + overallView: true, | ||
| 27 | + tilt: 30, | ||
| 28 | + duration, | ||
| 29 | + delay, | ||
| 30 | + }); | ||
| 31 | + trackAni?.start?.(); | ||
| 32 | + } catch (error) { | ||
| 33 | + throw error; | ||
| 34 | + } | ||
| 35 | + }; | ||
| 36 | + | ||
| 37 | + function marketPoint(params: Partial<Record<'lng' | 'lat', number>>) { | ||
| 38 | + const { lng, lat } = params; | ||
| 39 | + if (![lng, lat].every(Boolean)) return; | ||
| 40 | + const BMap = (window as any).BMapGL; | ||
| 41 | + if (!BMap) return; | ||
| 42 | + const marker = new BMap.Marker(new BMap.Point(lng, lat)); | ||
| 43 | + unref(mapInstance)?.centerAndZoom(new BMap.Point(lng, lat)); | ||
| 44 | + unref(mapInstance)?.addOverlay(marker); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + return { drawLine }; | ||
| 48 | +}; |
| 1 | +import { cloneDeep } from 'lodash-es'; | ||
| 2 | +import { Picture } from '.'; | ||
| 3 | +import { | ||
| 4 | + ConfigType, | ||
| 5 | + CreateComponentType, | ||
| 6 | + PublicComponentOptions, | ||
| 7 | + PublicPresetOptions, | ||
| 8 | +} from '../../../index.type'; | ||
| 9 | +import { PublicConfigClass, componentInitConfig } from '../../../publicConfig'; | ||
| 10 | + | ||
| 11 | +export const option: PublicPresetOptions = { | ||
| 12 | + componetDesign: false, | ||
| 13 | +}; | ||
| 14 | + | ||
| 15 | +export default class Config extends PublicConfigClass implements CreateComponentType { | ||
| 16 | + public key: string = Picture.key; | ||
| 17 | + | ||
| 18 | + public attr = { ...componentInitConfig }; | ||
| 19 | + | ||
| 20 | + public componentConfig: ConfigType = cloneDeep(Picture); | ||
| 21 | + | ||
| 22 | + public persetOption = cloneDeep(option); | ||
| 23 | + | ||
| 24 | + public option: PublicComponentOptions; | ||
| 25 | + | ||
| 26 | + constructor(option: PublicComponentOptions) { | ||
| 27 | + super(); | ||
| 28 | + this.option = { ...option }; | ||
| 29 | + } | ||
| 30 | +} |
| 1 | +<script lang="ts" setup> | ||
| 2 | + // import { computed } from 'vue'; | ||
| 3 | + import { commonDataSourceSchemas } from '../../../config/common.config'; | ||
| 4 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
| 5 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 6 | + | ||
| 7 | + // const props = defineProps<{ | ||
| 8 | + // values: PublicComponentDataSourceProps; | ||
| 9 | + // }>(); | ||
| 10 | + | ||
| 11 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | ||
| 12 | + labelWidth: 0, | ||
| 13 | + showActionButtonGroup: false, | ||
| 14 | + layout: 'horizontal', | ||
| 15 | + labelCol: { span: 0 }, | ||
| 16 | + schemas: commonDataSourceSchemas(), | ||
| 17 | + }); | ||
| 18 | + | ||
| 19 | + // const getBindValues = computed(() => { | ||
| 20 | + // return props.values.bindValue; | ||
| 21 | + // }); | ||
| 22 | + | ||
| 23 | + const getFormValues = () => { | ||
| 24 | + return getFieldsValue(); | ||
| 25 | + }; | ||
| 26 | + | ||
| 27 | + const setFormValues = (record: Recordable) => { | ||
| 28 | + return setFieldsValue(record); | ||
| 29 | + }; | ||
| 30 | + | ||
| 31 | + defineExpose({ | ||
| 32 | + getFormValues, | ||
| 33 | + setFormValues, | ||
| 34 | + validate, | ||
| 35 | + resetFormValues: resetFields, | ||
| 36 | + } as PublicFormInstaceType); | ||
| 37 | +</script> | ||
| 38 | + | ||
| 39 | +<template> | ||
| 40 | + <BasicForm @register="register" /> | ||
| 41 | +</template> |
| 1 | +import { useComponentKeys } from '../../../hook/useComponentKeys'; | ||
| 2 | +import { ConfigType, PackagesCategoryEnum } from '../../../index.type'; | ||
| 3 | + | ||
| 4 | +const componentKeys = useComponentKeys('Picture'); | ||
| 5 | +export const Picture: ConfigType = { | ||
| 6 | + ...componentKeys, | ||
| 7 | + title: '图片组件', | ||
| 8 | + package: PackagesCategoryEnum.PICTURE, | ||
| 9 | +}; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ref } from 'vue'; | ||
| 3 | + import { Image as AntImage } from 'ant-design-vue'; | ||
| 4 | + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime'; | ||
| 5 | + import { useDataFetch } from '../../../hook/useSocket'; | ||
| 6 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '../../../index.type'; | ||
| 7 | + | ||
| 8 | + const props = defineProps<{ | ||
| 9 | + config: ComponentPropsConfigType; | ||
| 10 | + }>(); | ||
| 11 | + | ||
| 12 | + const fallback = | ||
| 13 | + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg=='; | ||
| 14 | + | ||
| 15 | + const time = ref<Nullable<number>>(null); | ||
| 16 | + | ||
| 17 | + const url = ref<string>(fallback); | ||
| 18 | + | ||
| 19 | + // const getImagBase64 = ref(fallback); | ||
| 20 | + | ||
| 21 | + // const getBase64Image = (url: string) => { | ||
| 22 | + // let canvas: Nullable<HTMLCanvasElement> = document.createElement('canvas'); | ||
| 23 | + // const ctx = canvas.getContext('2d'); | ||
| 24 | + // let image: Nullable<HTMLImageElement> = new Image(); | ||
| 25 | + | ||
| 26 | + // image.onload = function () { | ||
| 27 | + // canvas!.height = image!.height; | ||
| 28 | + // canvas!.width = image!.width; | ||
| 29 | + // ctx?.drawImage(image!, 0, 0); | ||
| 30 | + // const dataUrl = canvas!.toDataURL('image/png'); | ||
| 31 | + // getImagBase64.value = dataUrl; | ||
| 32 | + // console.log(dataUrl); | ||
| 33 | + // image = null; | ||
| 34 | + // canvas = null; | ||
| 35 | + // }; | ||
| 36 | + // image.setAttribute('crossOrigin', 'Anonymous'); | ||
| 37 | + // image.src = url; | ||
| 38 | + // }; | ||
| 39 | + | ||
| 40 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | ||
| 41 | + const { data = {} } = message; | ||
| 42 | + const [latest] = data[attribute] || []; | ||
| 43 | + const [timespan, value] = latest; | ||
| 44 | + time.value = timespan; | ||
| 45 | + url.value = `${value}?timespan=${timespan}`; | ||
| 46 | + }; | ||
| 47 | + | ||
| 48 | + useDataFetch(props, updateFn); | ||
| 49 | +</script> | ||
| 50 | + | ||
| 51 | +<template> | ||
| 52 | + <main class="w-full h-full flex flex-col items-center"> | ||
| 53 | + <AntImage :src="url" :fallback="fallback" /> | ||
| 54 | + <UpdateTime :time="time" /> | ||
| 55 | + </main> | ||
| 56 | +</template> | ||
| 57 | + | ||
| 58 | +<style scoped lang="less"> | ||
| 59 | + :deep(.ant-image) { | ||
| 60 | + @apply flex justify-center w-auto; | ||
| 61 | + | ||
| 62 | + height: calc(100% - 56px); | ||
| 63 | + | ||
| 64 | + img { | ||
| 65 | + @apply w-auto h-full; | ||
| 66 | + } | ||
| 67 | + } | ||
| 68 | +</style> |
| 1 | +import cloneDeep from 'lodash-es/cloneDeep'; | ||
| 2 | +import { TextComponent1Config } from '.'; | ||
| 3 | +import { | ||
| 4 | + ConfigType, | ||
| 5 | + CreateComponentType, | ||
| 6 | + PublicComponentOptions, | ||
| 7 | + PublicPresetOptions, | ||
| 8 | +} from '/@/views/visual/packages/index.type'; | ||
| 9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | ||
| 10 | +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 11 | + | ||
| 12 | +export const option: PublicPresetOptions = { | ||
| 13 | + [ComponentConfigFieldEnum.FONT_COLOR]: '#000', | ||
| 14 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false, | ||
| 15 | +}; | ||
| 16 | + | ||
| 17 | +export default class Config extends PublicConfigClass implements CreateComponentType { | ||
| 18 | + public key: string = TextComponent1Config.key; | ||
| 19 | + | ||
| 20 | + public attr = { ...componentInitConfig }; | ||
| 21 | + | ||
| 22 | + public componentConfig: ConfigType = cloneDeep(TextComponent1Config); | ||
| 23 | + | ||
| 24 | + public persetOption = cloneDeep(option); | ||
| 25 | + | ||
| 26 | + public option: PublicComponentOptions; | ||
| 27 | + | ||
| 28 | + constructor(option: PublicComponentOptions) { | ||
| 29 | + super(); | ||
| 30 | + this.option = { ...option }; | ||
| 31 | + } | ||
| 32 | +} |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 3 | + import { useForm, BasicForm } from '/@/components/Form'; | ||
| 4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 5 | + | ||
| 6 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | ||
| 7 | + schemas: [ | ||
| 8 | + { | ||
| 9 | + field: ComponentConfigFieldEnum.FONT_COLOR, | ||
| 10 | + label: '数值字体颜色', | ||
| 11 | + component: 'ColorPicker', | ||
| 12 | + changeEvent: 'update:value', | ||
| 13 | + componentProps: { | ||
| 14 | + defaultValue: '#000', | ||
| 15 | + }, | ||
| 16 | + }, | ||
| 17 | + { | ||
| 18 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | ||
| 19 | + label: '显示设备名称', | ||
| 20 | + component: 'Checkbox', | ||
| 21 | + }, | ||
| 22 | + ], | ||
| 23 | + showActionButtonGroup: false, | ||
| 24 | + labelWidth: 120, | ||
| 25 | + baseColProps: { | ||
| 26 | + span: 12, | ||
| 27 | + }, | ||
| 28 | + }); | ||
| 29 | + | ||
| 30 | + const getFormValues = () => { | ||
| 31 | + return getFieldsValue(); | ||
| 32 | + }; | ||
| 33 | + | ||
| 34 | + const setFormValues = (data: Recordable) => { | ||
| 35 | + return setFieldsValue(data); | ||
| 36 | + }; | ||
| 37 | + | ||
| 38 | + defineExpose({ | ||
| 39 | + getFormValues, | ||
| 40 | + setFormValues, | ||
| 41 | + resetFormValues: resetFields, | ||
| 42 | + } as PublicFormInstaceType); | ||
| 43 | +</script> | ||
| 44 | + | ||
| 45 | +<template> | ||
| 46 | + <BasicForm @register="register" /> | ||
| 47 | +</template> |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { commonDataSourceSchemas } from '../../../config/common.config'; | ||
| 3 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
| 4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 5 | + | ||
| 6 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | ||
| 7 | + labelWidth: 0, | ||
| 8 | + showActionButtonGroup: false, | ||
| 9 | + layout: 'horizontal', | ||
| 10 | + labelCol: { span: 0 }, | ||
| 11 | + schemas: commonDataSourceSchemas(), | ||
| 12 | + }); | ||
| 13 | + | ||
| 14 | + const getFormValues = () => { | ||
| 15 | + return getFieldsValue(); | ||
| 16 | + }; | ||
| 17 | + | ||
| 18 | + const setFormValues = (record: Recordable) => { | ||
| 19 | + return setFieldsValue(record); | ||
| 20 | + }; | ||
| 21 | + | ||
| 22 | + defineExpose({ | ||
| 23 | + getFormValues, | ||
| 24 | + setFormValues, | ||
| 25 | + validate, | ||
| 26 | + resetFormValues: resetFields, | ||
| 27 | + } as PublicFormInstaceType); | ||
| 28 | +</script> | ||
| 29 | + | ||
| 30 | +<template> | ||
| 31 | + <BasicForm @register="register" /> | ||
| 32 | +</template> |
| 1 | +// import { ComponentEnum, ComponentNameEnum } from '../index.type'; | ||
| 2 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | ||
| 3 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | ||
| 4 | + | ||
| 5 | +const componentKeys = useComponentKeys('TextComponent1'); | ||
| 6 | +export const TextComponent1Config: ConfigType = { | ||
| 7 | + ...componentKeys, | ||
| 8 | + // title: ComponentNameEnum.TEXT_COMPONENT_1, | ||
| 9 | + title: '文本组件1', | ||
| 10 | + package: PackagesCategoryEnum.TEXT, | ||
| 11 | +}; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { option } from './config'; | ||
| 4 | + import { useComponentScale } from '/@/views/visual/packages/hook/useComponentScale'; | ||
| 5 | + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket'; | ||
| 6 | + import { computed } from 'vue'; | ||
| 7 | + import { ref } from 'vue'; | ||
| 8 | + import { DeviceName } from '/@/views/visual/commonComponents/DeviceName'; | ||
| 9 | + | ||
| 10 | + const props = defineProps<{ | ||
| 11 | + config: ComponentPropsConfigType<typeof option>; | ||
| 12 | + }>(); | ||
| 13 | + | ||
| 14 | + const currentValue = ref<string | number>(123); | ||
| 15 | + | ||
| 16 | + const getDesign = computed(() => { | ||
| 17 | + const { persetOption = {}, option } = props.config; | ||
| 18 | + | ||
| 19 | + const { fontColor: persetFontColor } = persetOption; | ||
| 20 | + | ||
| 21 | + const { componentInfo, attribute, attributeRename } = option; | ||
| 22 | + | ||
| 23 | + const { fontColor } = componentInfo || {}; | ||
| 24 | + return { | ||
| 25 | + fontColor: fontColor || persetFontColor, | ||
| 26 | + attribute: attributeRename || attribute, | ||
| 27 | + }; | ||
| 28 | + }); | ||
| 29 | + | ||
| 30 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | ||
| 31 | + const { data = {} } = message; | ||
| 32 | + const [latest] = data[attribute] || []; | ||
| 33 | + const [_, value] = latest; | ||
| 34 | + currentValue.value = value; | ||
| 35 | + }; | ||
| 36 | + | ||
| 37 | + useDataFetch(props, updateFn); | ||
| 38 | + | ||
| 39 | + const { getScale } = useComponentScale(props); | ||
| 40 | +</script> | ||
| 41 | + | ||
| 42 | +<template> | ||
| 43 | + <main :style="getScale" class="w-full h-full flex flex-col justify-center items-center"> | ||
| 44 | + <DeviceName :config="config" /> | ||
| 45 | + | ||
| 46 | + <h1 class="my-4 font-bold text-xl !my-2 truncate" :style="{ color: getDesign.fontColor }"> | ||
| 47 | + {{ currentValue || 0 }} | ||
| 48 | + </h1> | ||
| 49 | + <div class="text-gray-500 text-lg truncate">{{ getDesign.attribute || '温度' }}</div> | ||
| 50 | + <!-- <div v-if="config.option.componentInfo.showDeviceName"> | ||
| 51 | + {{ config.option.deviceRename || config.option.deviceName }} | ||
| 52 | + </div> --> | ||
| 53 | + </main> | ||
| 54 | +</template> |
| 1 | +import cloneDeep from 'lodash-es/cloneDeep'; | ||
| 2 | +import { TextComponent2Config } from '.'; | ||
| 3 | +import { | ||
| 4 | + ConfigType, | ||
| 5 | + CreateComponentType, | ||
| 6 | + PublicComponentOptions, | ||
| 7 | + PublicPresetOptions, | ||
| 8 | +} from '/@/views/visual/packages/index.type'; | ||
| 9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | ||
| 10 | +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 11 | + | ||
| 12 | +export const option: PublicPresetOptions = { | ||
| 13 | + [ComponentConfigFieldEnum.FONT_COLOR]: '#000', | ||
| 14 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false, | ||
| 15 | +}; | ||
| 16 | + | ||
| 17 | +export default class Config extends PublicConfigClass implements CreateComponentType { | ||
| 18 | + public key: string = TextComponent2Config.key; | ||
| 19 | + | ||
| 20 | + public attr = { ...componentInitConfig }; | ||
| 21 | + | ||
| 22 | + public componentConfig: ConfigType = cloneDeep(TextComponent2Config); | ||
| 23 | + | ||
| 24 | + public persetOption = cloneDeep(option); | ||
| 25 | + | ||
| 26 | + public option: PublicComponentOptions; | ||
| 27 | + | ||
| 28 | + constructor(option: PublicComponentOptions) { | ||
| 29 | + super(); | ||
| 30 | + this.option = { ...option }; | ||
| 31 | + } | ||
| 32 | +} |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 3 | + import { useForm, BasicForm } from '/@/components/Form'; | ||
| 4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 5 | + | ||
| 6 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | ||
| 7 | + schemas: [ | ||
| 8 | + { | ||
| 9 | + field: ComponentConfigFieldEnum.FONT_COLOR, | ||
| 10 | + label: '数值字体颜色', | ||
| 11 | + component: 'ColorPicker', | ||
| 12 | + changeEvent: 'update:value', | ||
| 13 | + componentProps: { | ||
| 14 | + defaultValue: '#000', | ||
| 15 | + }, | ||
| 16 | + }, | ||
| 17 | + { | ||
| 18 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | ||
| 19 | + label: '显示设备名称', | ||
| 20 | + component: 'Checkbox', | ||
| 21 | + }, | ||
| 22 | + ], | ||
| 23 | + showActionButtonGroup: false, | ||
| 24 | + labelWidth: 120, | ||
| 25 | + baseColProps: { | ||
| 26 | + span: 12, | ||
| 27 | + }, | ||
| 28 | + }); | ||
| 29 | + | ||
| 30 | + const getFormValues = () => { | ||
| 31 | + return getFieldsValue(); | ||
| 32 | + }; | ||
| 33 | + | ||
| 34 | + const setFormValues = (data: Recordable) => { | ||
| 35 | + return setFieldsValue(data); | ||
| 36 | + }; | ||
| 37 | + | ||
| 38 | + defineExpose({ | ||
| 39 | + getFormValues, | ||
| 40 | + setFormValues, | ||
| 41 | + resetFormValues: resetFields, | ||
| 42 | + } as PublicFormInstaceType); | ||
| 43 | +</script> | ||
| 44 | + | ||
| 45 | +<template> | ||
| 46 | + <BasicForm @register="register" /> | ||
| 47 | +</template> |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { commonDataSourceSchemas } from '../../../config/common.config'; | ||
| 3 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
| 4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 5 | + | ||
| 6 | + // const props = defineProps<{ | ||
| 7 | + // values: PublicComponentDataSourceProps; | ||
| 8 | + // }>(); | ||
| 9 | + | ||
| 10 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | ||
| 11 | + labelWidth: 0, | ||
| 12 | + showActionButtonGroup: false, | ||
| 13 | + layout: 'horizontal', | ||
| 14 | + labelCol: { span: 0 }, | ||
| 15 | + schemas: commonDataSourceSchemas(), | ||
| 16 | + }); | ||
| 17 | + | ||
| 18 | + const getFormValues = () => { | ||
| 19 | + return getFieldsValue(); | ||
| 20 | + }; | ||
| 21 | + | ||
| 22 | + const setFormValues = (record: Recordable) => { | ||
| 23 | + return setFieldsValue(record); | ||
| 24 | + }; | ||
| 25 | + | ||
| 26 | + defineExpose({ | ||
| 27 | + getFormValues, | ||
| 28 | + setFormValues, | ||
| 29 | + validate, | ||
| 30 | + resetFormValues: resetFields, | ||
| 31 | + } as PublicFormInstaceType); | ||
| 32 | +</script> | ||
| 33 | + | ||
| 34 | +<template> | ||
| 35 | + <BasicForm @register="register" /> | ||
| 36 | +</template> |
| 1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | ||
| 2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | ||
| 3 | + | ||
| 4 | +const componentKeys = useComponentKeys('TextComponent2'); | ||
| 5 | +export const TextComponent2Config: ConfigType = { | ||
| 6 | + ...componentKeys, | ||
| 7 | + title: '文本组件2', | ||
| 8 | + package: PackagesCategoryEnum.TEXT, | ||
| 9 | +}; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { option } from './config'; | ||
| 4 | + import { useComponentScale } from '/@/views/visual/packages/hook/useComponentScale'; | ||
| 5 | + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket'; | ||
| 6 | + import { computed } from 'vue'; | ||
| 7 | + import { ref } from 'vue'; | ||
| 8 | + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime'; | ||
| 9 | + import { DeviceName } from '/@/views/visual/commonComponents/DeviceName'; | ||
| 10 | + | ||
| 11 | + const props = defineProps<{ | ||
| 12 | + config: ComponentPropsConfigType<typeof option>; | ||
| 13 | + }>(); | ||
| 14 | + | ||
| 15 | + const currentValue = ref<string | number>(123); | ||
| 16 | + const time = ref<Nullable<number>>(null); | ||
| 17 | + | ||
| 18 | + const getDesign = computed(() => { | ||
| 19 | + const { persetOption = {}, option } = props.config; | ||
| 20 | + | ||
| 21 | + const { fontColor: persetFontColor } = persetOption; | ||
| 22 | + | ||
| 23 | + const { componentInfo, attribute, attributeRename } = option; | ||
| 24 | + | ||
| 25 | + const { fontColor } = componentInfo || {}; | ||
| 26 | + | ||
| 27 | + return { | ||
| 28 | + fontColor: fontColor || persetFontColor, | ||
| 29 | + attribute: attributeRename || attribute, | ||
| 30 | + }; | ||
| 31 | + }); | ||
| 32 | + | ||
| 33 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | ||
| 34 | + const { data = {} } = message; | ||
| 35 | + const [info] = data[attribute] || []; | ||
| 36 | + const [timespan, value] = info; | ||
| 37 | + currentValue.value = value; | ||
| 38 | + time.value = timespan; | ||
| 39 | + }; | ||
| 40 | + | ||
| 41 | + useDataFetch(props, updateFn); | ||
| 42 | + | ||
| 43 | + const { getScale } = useComponentScale(props); | ||
| 44 | +</script> | ||
| 45 | + | ||
| 46 | +<template> | ||
| 47 | + <main class="w-full h-full flex flex-col justify-center items-center"> | ||
| 48 | + <DeviceName :config="config" /> | ||
| 49 | + | ||
| 50 | + <div class="flex-1 flex justify-center items-center flex-col" :style="getScale"> | ||
| 51 | + <h1 class="my-4 font-bold text-xl !my-2 truncate" :style="{ color: getDesign.fontColor }"> | ||
| 52 | + {{ currentValue || 0 }} | ||
| 53 | + </h1> | ||
| 54 | + <div class="text-gray-500 text-lg truncate">{{ getDesign.attribute || '温度' }}</div> | ||
| 55 | + </div> | ||
| 56 | + <UpdateTime :time="time" /> | ||
| 57 | + </main> | ||
| 58 | +</template> |
| 1 | +import cloneDeep from 'lodash-es/cloneDeep'; | ||
| 2 | +import { TextComponent3Config } from '.'; | ||
| 3 | +import { | ||
| 4 | + ConfigType, | ||
| 5 | + CreateComponentType, | ||
| 6 | + PublicComponentOptions, | ||
| 7 | + PublicPresetOptions, | ||
| 8 | +} from '/@/views/visual/packages/index.type'; | ||
| 9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | ||
| 10 | +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 11 | + | ||
| 12 | +export const option: PublicPresetOptions = { | ||
| 13 | + [ComponentConfigFieldEnum.UNIT]: '℃', | ||
| 14 | + [ComponentConfigFieldEnum.ICON]: 'shuiwen', | ||
| 15 | + [ComponentConfigFieldEnum.ICON_COLOR]: '#367bff', | ||
| 16 | + [ComponentConfigFieldEnum.FONT_COLOR]: '#000', | ||
| 17 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false, | ||
| 18 | +}; | ||
| 19 | + | ||
| 20 | +export default class Config extends PublicConfigClass implements CreateComponentType { | ||
| 21 | + public key: string = TextComponent3Config.key; | ||
| 22 | + | ||
| 23 | + public attr = { ...componentInitConfig }; | ||
| 24 | + | ||
| 25 | + public componentConfig: ConfigType = cloneDeep(TextComponent3Config); | ||
| 26 | + | ||
| 27 | + public persetOption = cloneDeep(option); | ||
| 28 | + | ||
| 29 | + public option: PublicComponentOptions; | ||
| 30 | + | ||
| 31 | + constructor(option: PublicComponentOptions) { | ||
| 32 | + super(); | ||
| 33 | + this.option = { ...option }; | ||
| 34 | + } | ||
| 35 | +} |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 3 | + import { useForm, BasicForm } from '/@/components/Form'; | ||
| 4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 5 | + import { option } from './config'; | ||
| 6 | + | ||
| 7 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | ||
| 8 | + schemas: [ | ||
| 9 | + { | ||
| 10 | + field: ComponentConfigFieldEnum.FONT_COLOR, | ||
| 11 | + label: '数值字体颜色', | ||
| 12 | + component: 'ColorPicker', | ||
| 13 | + changeEvent: 'update:value', | ||
| 14 | + valueField: 'value', | ||
| 15 | + defaultValue: option.fontColor, | ||
| 16 | + }, | ||
| 17 | + { | ||
| 18 | + field: ComponentConfigFieldEnum.UNIT, | ||
| 19 | + label: '数值单位', | ||
| 20 | + component: 'Input', | ||
| 21 | + defaultValue: option.unit, | ||
| 22 | + componentProps: { | ||
| 23 | + placeholder: '请输入数值单位', | ||
| 24 | + }, | ||
| 25 | + }, | ||
| 26 | + { | ||
| 27 | + field: ComponentConfigFieldEnum.ICON_COLOR, | ||
| 28 | + label: '图标颜色', | ||
| 29 | + component: 'ColorPicker', | ||
| 30 | + changeEvent: 'update:value', | ||
| 31 | + defaultValue: option.iconColor, | ||
| 32 | + }, | ||
| 33 | + { | ||
| 34 | + field: ComponentConfigFieldEnum.ICON, | ||
| 35 | + label: '图标', | ||
| 36 | + component: 'IconDrawer', | ||
| 37 | + changeEvent: 'update:value', | ||
| 38 | + defaultValue: option.icon, | ||
| 39 | + componentProps({ formModel }) { | ||
| 40 | + const color = formModel[ComponentConfigFieldEnum.ICON_COLOR]; | ||
| 41 | + return { | ||
| 42 | + color, | ||
| 43 | + }; | ||
| 44 | + }, | ||
| 45 | + }, | ||
| 46 | + { | ||
| 47 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | ||
| 48 | + label: '显示设备名称', | ||
| 49 | + component: 'Checkbox', | ||
| 50 | + defaultValue: option.showDeviceName, | ||
| 51 | + }, | ||
| 52 | + ], | ||
| 53 | + showActionButtonGroup: false, | ||
| 54 | + labelWidth: 120, | ||
| 55 | + baseColProps: { | ||
| 56 | + span: 12, | ||
| 57 | + }, | ||
| 58 | + }); | ||
| 59 | + | ||
| 60 | + const getFormValues = () => { | ||
| 61 | + return getFieldsValue(); | ||
| 62 | + }; | ||
| 63 | + | ||
| 64 | + const setFormValues = (data: Recordable) => { | ||
| 65 | + return setFieldsValue(data); | ||
| 66 | + }; | ||
| 67 | + | ||
| 68 | + defineExpose({ | ||
| 69 | + getFormValues, | ||
| 70 | + setFormValues, | ||
| 71 | + resetFormValues: resetFields, | ||
| 72 | + } as PublicFormInstaceType); | ||
| 73 | +</script> | ||
| 74 | + | ||
| 75 | +<template> | ||
| 76 | + <BasicForm @register="register" /> | ||
| 77 | +</template> |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { commonDataSourceSchemas } from '../../../config/common.config'; | ||
| 3 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
| 4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 5 | + | ||
| 6 | + // const props = defineProps<{ | ||
| 7 | + // values: PublicComponentDataSourceProps; | ||
| 8 | + // }>(); | ||
| 9 | + | ||
| 10 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | ||
| 11 | + labelWidth: 0, | ||
| 12 | + showActionButtonGroup: false, | ||
| 13 | + layout: 'horizontal', | ||
| 14 | + labelCol: { span: 0 }, | ||
| 15 | + schemas: commonDataSourceSchemas(), | ||
| 16 | + }); | ||
| 17 | + | ||
| 18 | + const getFormValues = () => { | ||
| 19 | + return getFieldsValue(); | ||
| 20 | + }; | ||
| 21 | + | ||
| 22 | + const setFormValues = (record: Recordable) => { | ||
| 23 | + return setFieldsValue(record); | ||
| 24 | + }; | ||
| 25 | + | ||
| 26 | + defineExpose({ | ||
| 27 | + getFormValues, | ||
| 28 | + setFormValues, | ||
| 29 | + validate, | ||
| 30 | + resetFormValues: resetFields, | ||
| 31 | + } as PublicFormInstaceType); | ||
| 32 | +</script> | ||
| 33 | + | ||
| 34 | +<template> | ||
| 35 | + <BasicForm @register="register" /> | ||
| 36 | +</template> |
| 1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | ||
| 2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | ||
| 3 | + | ||
| 4 | +const componentKeys = useComponentKeys('TextComponent3'); | ||
| 5 | +export const TextComponent3Config: ConfigType = { | ||
| 6 | + ...componentKeys, | ||
| 7 | + title: '文本组件3', | ||
| 8 | + package: PackagesCategoryEnum.TEXT, | ||
| 9 | +}; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { option } from './config'; | ||
| 4 | + import { useComponentScale } from '/@/views/visual/packages/hook/useComponentScale'; | ||
| 5 | + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket'; | ||
| 6 | + import { ref } from 'vue'; | ||
| 7 | + import { UpdateTime } from '/@/views/visual/commonComponents/UpdateTime'; | ||
| 8 | + import { SvgIcon } from '/@/components/Icon'; | ||
| 9 | + import { computed } from 'vue'; | ||
| 10 | + import { DeviceName } from '/@/views/visual/commonComponents/DeviceName'; | ||
| 11 | + | ||
| 12 | + const props = defineProps<{ | ||
| 13 | + config: ComponentPropsConfigType<typeof option>; | ||
| 14 | + }>(); | ||
| 15 | + | ||
| 16 | + const currentValue = ref<string | number>(123); | ||
| 17 | + | ||
| 18 | + const time = ref<Nullable<number>>(null); | ||
| 19 | + | ||
| 20 | + const getDesign = computed(() => { | ||
| 21 | + const { persetOption = {}, option } = props.config; | ||
| 22 | + | ||
| 23 | + const { | ||
| 24 | + iconColor: persetIconColor, | ||
| 25 | + unit: perseUnit, | ||
| 26 | + icon: persetIcon, | ||
| 27 | + fontColor: persetFontColor, | ||
| 28 | + } = persetOption; | ||
| 29 | + | ||
| 30 | + const { componentInfo, attribute, attributeRename } = option; | ||
| 31 | + | ||
| 32 | + const { icon, iconColor, fontColor, unit } = componentInfo || {}; | ||
| 33 | + return { | ||
| 34 | + iconColor: iconColor || persetIconColor, | ||
| 35 | + unit: unit ?? perseUnit, | ||
| 36 | + icon: icon || persetIcon, | ||
| 37 | + fontColor: fontColor || persetFontColor, | ||
| 38 | + attribute: attributeRename || attribute, | ||
| 39 | + }; | ||
| 40 | + }); | ||
| 41 | + | ||
| 42 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | ||
| 43 | + const { data = {} } = message; | ||
| 44 | + const [latest] = data[attribute] || []; | ||
| 45 | + const [timespan, value] = latest; | ||
| 46 | + currentValue.value = value; | ||
| 47 | + time.value = timespan; | ||
| 48 | + }; | ||
| 49 | + | ||
| 50 | + useDataFetch(props, updateFn); | ||
| 51 | + | ||
| 52 | + const { getScale } = useComponentScale(props); | ||
| 53 | +</script> | ||
| 54 | + | ||
| 55 | +<template> | ||
| 56 | + <main :style="getScale" class="w-full h-full flex flex-col justify-center items-center"> | ||
| 57 | + <DeviceName :config="config" /> | ||
| 58 | + <div class="flex-1 flex justify-center items-center flex-col"> | ||
| 59 | + <SvgIcon | ||
| 60 | + :name="getDesign.icon!" | ||
| 61 | + prefix="iconfont" | ||
| 62 | + :size="60" | ||
| 63 | + :style="{ color: getDesign.iconColor }" | ||
| 64 | + /> | ||
| 65 | + <h1 class="font-bold text-xl m-2 truncate" :style="{ color: getDesign.fontColor }"> | ||
| 66 | + <span> {{ currentValue || 0 }}</span> | ||
| 67 | + <span>{{ getDesign.unit }} </span> | ||
| 68 | + </h1> | ||
| 69 | + <div class="text-gray-500 text-lg truncate">{{ getDesign.attribute || '温度' }}</div> | ||
| 70 | + </div> | ||
| 71 | + <UpdateTime :time="time" /> | ||
| 72 | + </main> | ||
| 73 | +</template> |
| 1 | +import cloneDeep from 'lodash-es/cloneDeep'; | ||
| 2 | +import { TextComponent4Config } from '.'; | ||
| 3 | +import { | ||
| 4 | + ConfigType, | ||
| 5 | + CreateComponentType, | ||
| 6 | + PublicComponentOptions, | ||
| 7 | + PublicPresetOptions, | ||
| 8 | +} from '/@/views/visual/packages/index.type'; | ||
| 9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | ||
| 10 | +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 11 | + | ||
| 12 | +export const option: PublicPresetOptions = { | ||
| 13 | + [ComponentConfigFieldEnum.UNIT]: '℃', | ||
| 14 | + [ComponentConfigFieldEnum.ICON]: 'shuiwen', | ||
| 15 | + [ComponentConfigFieldEnum.ICON_COLOR]: '#367bff', | ||
| 16 | + [ComponentConfigFieldEnum.FONT_COLOR]: '#000', | ||
| 17 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false, | ||
| 18 | +}; | ||
| 19 | + | ||
| 20 | +export default class Config extends PublicConfigClass implements CreateComponentType { | ||
| 21 | + public key: string = TextComponent4Config.key; | ||
| 22 | + | ||
| 23 | + public attr = { ...componentInitConfig }; | ||
| 24 | + | ||
| 25 | + public componentConfig: ConfigType = cloneDeep(TextComponent4Config); | ||
| 26 | + | ||
| 27 | + public persetOption = cloneDeep(option); | ||
| 28 | + | ||
| 29 | + public option: PublicComponentOptions; | ||
| 30 | + | ||
| 31 | + constructor(option: PublicComponentOptions) { | ||
| 32 | + super(); | ||
| 33 | + this.option = { ...option }; | ||
| 34 | + } | ||
| 35 | +} |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 3 | + import { useForm, BasicForm } from '/@/components/Form'; | ||
| 4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 5 | + import { option } from './config'; | ||
| 6 | + | ||
| 7 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | ||
| 8 | + schemas: [ | ||
| 9 | + { | ||
| 10 | + field: ComponentConfigFieldEnum.FONT_COLOR, | ||
| 11 | + label: '数值字体颜色', | ||
| 12 | + component: 'ColorPicker', | ||
| 13 | + changeEvent: 'update:value', | ||
| 14 | + valueField: 'value', | ||
| 15 | + defaultValue: option.fontColor, | ||
| 16 | + }, | ||
| 17 | + { | ||
| 18 | + field: ComponentConfigFieldEnum.UNIT, | ||
| 19 | + label: '数值单位', | ||
| 20 | + component: 'Input', | ||
| 21 | + defaultValue: option.unit, | ||
| 22 | + componentProps: { | ||
| 23 | + placeholder: '请输入数值单位', | ||
| 24 | + }, | ||
| 25 | + }, | ||
| 26 | + { | ||
| 27 | + field: ComponentConfigFieldEnum.ICON_COLOR, | ||
| 28 | + label: '图标颜色', | ||
| 29 | + component: 'ColorPicker', | ||
| 30 | + changeEvent: 'update:value', | ||
| 31 | + defaultValue: option.iconColor, | ||
| 32 | + }, | ||
| 33 | + { | ||
| 34 | + field: ComponentConfigFieldEnum.ICON, | ||
| 35 | + label: '图标', | ||
| 36 | + component: 'IconDrawer', | ||
| 37 | + changeEvent: 'update:value', | ||
| 38 | + defaultValue: option.icon, | ||
| 39 | + componentProps({ formModel }) { | ||
| 40 | + const color = formModel[ComponentConfigFieldEnum.ICON_COLOR]; | ||
| 41 | + return { | ||
| 42 | + color, | ||
| 43 | + }; | ||
| 44 | + }, | ||
| 45 | + }, | ||
| 46 | + { | ||
| 47 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | ||
| 48 | + label: '显示设备名称', | ||
| 49 | + component: 'Checkbox', | ||
| 50 | + defaultValue: option.showDeviceName, | ||
| 51 | + }, | ||
| 52 | + ], | ||
| 53 | + showActionButtonGroup: false, | ||
| 54 | + labelWidth: 120, | ||
| 55 | + baseColProps: { | ||
| 56 | + span: 12, | ||
| 57 | + }, | ||
| 58 | + }); | ||
| 59 | + | ||
| 60 | + const getFormValues = () => { | ||
| 61 | + return getFieldsValue(); | ||
| 62 | + }; | ||
| 63 | + | ||
| 64 | + const setFormValues = (data: Recordable) => { | ||
| 65 | + return setFieldsValue(data); | ||
| 66 | + }; | ||
| 67 | + | ||
| 68 | + defineExpose({ | ||
| 69 | + getFormValues, | ||
| 70 | + setFormValues, | ||
| 71 | + resetFormValues: resetFields, | ||
| 72 | + } as PublicFormInstaceType); | ||
| 73 | +</script> | ||
| 74 | + | ||
| 75 | +<template> | ||
| 76 | + <BasicForm @register="register" /> | ||
| 77 | +</template> |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { commonDataSourceSchemas } from '../../../config/common.config'; | ||
| 3 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
| 4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 5 | + | ||
| 6 | + // const props = defineProps<{ | ||
| 7 | + // values: PublicComponentDataSourceProps; | ||
| 8 | + // }>(); | ||
| 9 | + | ||
| 10 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | ||
| 11 | + labelWidth: 0, | ||
| 12 | + showActionButtonGroup: false, | ||
| 13 | + layout: 'horizontal', | ||
| 14 | + labelCol: { span: 0 }, | ||
| 15 | + schemas: commonDataSourceSchemas(), | ||
| 16 | + }); | ||
| 17 | + | ||
| 18 | + const getFormValues = () => { | ||
| 19 | + return getFieldsValue(); | ||
| 20 | + }; | ||
| 21 | + | ||
| 22 | + const setFormValues = (record: Recordable) => { | ||
| 23 | + return setFieldsValue(record); | ||
| 24 | + }; | ||
| 25 | + | ||
| 26 | + defineExpose({ | ||
| 27 | + getFormValues, | ||
| 28 | + setFormValues, | ||
| 29 | + validate, | ||
| 30 | + resetFormValues: resetFields, | ||
| 31 | + } as PublicFormInstaceType); | ||
| 32 | +</script> | ||
| 33 | + | ||
| 34 | +<template> | ||
| 35 | + <BasicForm @register="register" /> | ||
| 36 | +</template> |
| 1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | ||
| 2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | ||
| 3 | + | ||
| 4 | +const componentKeys = useComponentKeys('TextComponent4'); | ||
| 5 | +export const TextComponent4Config: ConfigType = { | ||
| 6 | + ...componentKeys, | ||
| 7 | + title: '文本组件4', | ||
| 8 | + package: PackagesCategoryEnum.TEXT, | ||
| 9 | +}; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { option } from './config'; | ||
| 4 | + import { useComponentScale } from '/@/views/visual/packages/hook/useComponentScale'; | ||
| 5 | + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket'; | ||
| 6 | + import { computed } from 'vue'; | ||
| 7 | + import { ref } from 'vue'; | ||
| 8 | + import { SvgIcon } from '/@/components/Icon'; | ||
| 9 | + import { DeviceName } from '/@/views/visual/commonComponents/DeviceName'; | ||
| 10 | + | ||
| 11 | + const props = defineProps<{ | ||
| 12 | + config: ComponentPropsConfigType<typeof option>; | ||
| 13 | + }>(); | ||
| 14 | + | ||
| 15 | + const currentValue = ref<string | number>(123); | ||
| 16 | + | ||
| 17 | + const getDesign = computed(() => { | ||
| 18 | + const { persetOption = {}, option } = props.config; | ||
| 19 | + | ||
| 20 | + const { | ||
| 21 | + iconColor: persetIconColor, | ||
| 22 | + unit: perseUnit, | ||
| 23 | + icon: persetIcon, | ||
| 24 | + fontColor: persetFontColor, | ||
| 25 | + } = persetOption; | ||
| 26 | + | ||
| 27 | + const { componentInfo, attribute, attributeRename } = option; | ||
| 28 | + | ||
| 29 | + const { icon, iconColor, fontColor, unit } = componentInfo || {}; | ||
| 30 | + return { | ||
| 31 | + iconColor: iconColor || persetIconColor, | ||
| 32 | + unit: unit ?? perseUnit, | ||
| 33 | + icon: icon || persetIcon, | ||
| 34 | + fontColor: fontColor || persetFontColor, | ||
| 35 | + attribute: attributeRename || attribute, | ||
| 36 | + }; | ||
| 37 | + }); | ||
| 38 | + | ||
| 39 | + const updateFn: DataFetchUpdateFn = (message, attribute) => { | ||
| 40 | + const { data = {} } = message; | ||
| 41 | + const [info] = data[attribute] || []; | ||
| 42 | + const [_, value] = info; | ||
| 43 | + currentValue.value = value; | ||
| 44 | + }; | ||
| 45 | + | ||
| 46 | + useDataFetch(props, updateFn); | ||
| 47 | + | ||
| 48 | + const { getScale } = useComponentScale(props); | ||
| 49 | +</script> | ||
| 50 | + | ||
| 51 | +<template> | ||
| 52 | + <main class="w-full h-full flex flex-col justify-center items-center"> | ||
| 53 | + <DeviceName :config="config" /> | ||
| 54 | + <div :style="getScale" class="flex-1 flex justify-center items-center flex-col"> | ||
| 55 | + <SvgIcon | ||
| 56 | + :name="getDesign.icon!" | ||
| 57 | + prefix="iconfont" | ||
| 58 | + :size="60" | ||
| 59 | + :style="{ color: getDesign.iconColor }" | ||
| 60 | + /> | ||
| 61 | + <h1 class="my-4 font-bold text-lg !my-2 truncate" :style="{ color: getDesign.fontColor }"> | ||
| 62 | + <span>{{ currentValue || 0 }}</span> | ||
| 63 | + <span>{{ getDesign.unit }}</span> | ||
| 64 | + </h1> | ||
| 65 | + <div class="text-gray-500 text-lg truncate">{{ getDesign.attribute || '温度' }}</div> | ||
| 66 | + </div> | ||
| 67 | + </main> | ||
| 68 | +</template> |
| 1 | +import { TextComponent1Config } from './TextComponent1'; | ||
| 2 | +import { TextComponent2Config } from './TextComponent2'; | ||
| 3 | +import { TextComponent3Config } from './TextComponent3'; | ||
| 4 | +import { TextComponent4Config } from './TextComponent4'; | ||
| 5 | + | ||
| 6 | +export const TextList = [ | ||
| 7 | + TextComponent1Config, | ||
| 8 | + TextComponent2Config, | ||
| 9 | + TextComponent3Config, | ||
| 10 | + TextComponent4Config, | ||
| 11 | +]; |
| 1 | +export enum ComponentEnum { | ||
| 2 | + TEXT_COMPONENT_1 = 'TEXT_COMPONENT_1', | ||
| 3 | + TEXT_COMPONENT_2 = 'TEXT_COMPONENT_2', | ||
| 4 | + TEXT_COMPONENT_3 = 'TEXT_COMPONENT_3', | ||
| 5 | + TEXT_COMPONENT_4 = 'TEXT_COMPONENT_4', | ||
| 6 | + TEXT_COMPONENT_5 = 'TEXT_COMPONENT_5', | ||
| 7 | +} | ||
| 8 | + | ||
| 9 | +export enum ComponentNameEnum { | ||
| 10 | + TEXT_COMPONENT_1 = '文本组件1', | ||
| 11 | + TEXT_COMPONENT_2 = '文本组件2', | ||
| 12 | + TEXT_COMPONENT_3 = '文本组件3', | ||
| 13 | + TEXT_COMPONENT_4 = '文本组件4', | ||
| 14 | + TEXT_COMPONENT_5 = '文本组件5', | ||
| 15 | +} |
| 1 | +import { unref } from 'vue'; | ||
| 2 | +import { useSelectWidgetKeys, useSelectWidgetMode } from '../../dataSourceBindPanel/useContext'; | ||
| 3 | +import { PackagesCategoryEnum } from '../index.type'; | ||
| 4 | +import { getDeviceProfile } from '/@/api/alarm/position'; | ||
| 5 | +import { getDeviceAttributes, getMeetTheConditionsDevice } from '/@/api/dataBoard'; | ||
| 6 | +import { DeviceAttributeParams, MasterDeviceList } from '/@/api/dataBoard/model'; | ||
| 7 | +import { DeviceTypeEnum } from '/@/api/device/model/deviceModel'; | ||
| 8 | +import { ModelOfMatterParams } from '/@/api/device/model/modelOfMatterModel'; | ||
| 9 | +import { getModelServices } from '/@/api/device/modelOfMatter'; | ||
| 10 | +import { findDictItemByCode } from '/@/api/system/dict'; | ||
| 11 | +import { FormSchema, useComponentRegister } from '/@/components/Form'; | ||
| 12 | +import { DataTypeEnum } from '/@/components/Form/src/externalCompns/components/StructForm/config'; | ||
| 13 | +import { OrgTreeSelect } from '/@/views/common/OrgTreeSelect'; | ||
| 14 | +import { TransportTypeEnum } from '/@/views/device/profiles/components/TransportDescript/const'; | ||
| 15 | +import { CommandTypeEnum } from '/@/views/rule/linkedge/config/config.data'; | ||
| 16 | +import { DataActionModeEnum } from '/@/enums/toolEnum'; | ||
| 17 | + | ||
| 18 | +useComponentRegister('OrgTreeSelect', OrgTreeSelect); | ||
| 19 | + | ||
| 20 | +export interface CommonDataSourceBindValueType extends Record<DataSourceField, string> { | ||
| 21 | + customCommand?: { | ||
| 22 | + transportType?: string; | ||
| 23 | + service?: string; | ||
| 24 | + command?: string; | ||
| 25 | + commandType?: string; | ||
| 26 | + }; | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +export enum DataSourceField { | ||
| 30 | + IS_GATEWAY_DEVICE = 'gatewayDevice', | ||
| 31 | + DEVICE_TYPE = 'deviceType', | ||
| 32 | + TRANSPORT_TYPE = 'transportType', | ||
| 33 | + ORIGINATION_ID = 'organizationId', | ||
| 34 | + DEVICE_ID = 'deviceId', | ||
| 35 | + DEVICE_PROFILE_ID = 'deviceProfileId', | ||
| 36 | + ATTRIBUTE = 'attribute', | ||
| 37 | + ATTRIBUTE_RENAME = 'attributeRename', | ||
| 38 | + DEVICE_NAME = 'deviceName', | ||
| 39 | + DEVICE_RENAME = 'deviceRename', | ||
| 40 | + LONGITUDE_ATTRIBUTE = 'longitudeAttribute', | ||
| 41 | + LATITUDE_ATTRIBUTE = 'latitudeAttribute', | ||
| 42 | + | ||
| 43 | + COMMAND = 'command', | ||
| 44 | + COMMAND_TYPE = 'commandType', | ||
| 45 | + SERVICE = 'service', | ||
| 46 | +} | ||
| 47 | + | ||
| 48 | +const isTcpProfile = (transportType: string) => transportType === TransportTypeEnum.TCP; | ||
| 49 | + | ||
| 50 | +const isControlComponent = (category?: string) => PackagesCategoryEnum.CONTROL === category; | ||
| 51 | + | ||
| 52 | +const getDeviceService = async (deviceProfileId: string) => { | ||
| 53 | + try { | ||
| 54 | + const data = await getModelServices({ deviceProfileId }); | ||
| 55 | + if (data) | ||
| 56 | + return data.map((item) => ({ ...item, label: item.functionName, value: item.identifier })); | ||
| 57 | + } catch (error) {} | ||
| 58 | + return []; | ||
| 59 | +}; | ||
| 60 | + | ||
| 61 | +const getDeviceAttribute = async (params: DeviceAttributeParams) => { | ||
| 62 | + try { | ||
| 63 | + const data = await getDeviceAttributes(params); | ||
| 64 | + if (data) return data.map((item) => ({ label: item.name, value: item.identifier })); | ||
| 65 | + } catch (error) {} | ||
| 66 | + return []; | ||
| 67 | +}; | ||
| 68 | + | ||
| 69 | +export const commonDataSourceSchemas = (): FormSchema[] => { | ||
| 70 | + const mode = useSelectWidgetMode(); | ||
| 71 | + const isUpdate = unref(mode) === DataActionModeEnum.UPDATE; | ||
| 72 | + const selectWidgetKeys = useSelectWidgetKeys(); | ||
| 73 | + const category = unref(selectWidgetKeys).categoryKey; | ||
| 74 | + | ||
| 75 | + return [ | ||
| 76 | + { | ||
| 77 | + field: DataSourceField.IS_GATEWAY_DEVICE, | ||
| 78 | + component: 'Switch', | ||
| 79 | + label: '是否是网关设备', | ||
| 80 | + show: false, | ||
| 81 | + }, | ||
| 82 | + { | ||
| 83 | + field: DataSourceField.DEVICE_NAME, | ||
| 84 | + component: 'Input', | ||
| 85 | + label: '设备名', | ||
| 86 | + show: false, | ||
| 87 | + }, | ||
| 88 | + { | ||
| 89 | + field: DataSourceField.TRANSPORT_TYPE, | ||
| 90 | + component: 'Input', | ||
| 91 | + label: '设备配置类型', | ||
| 92 | + show: false, | ||
| 93 | + }, | ||
| 94 | + { | ||
| 95 | + field: DataSourceField.DEVICE_TYPE, | ||
| 96 | + component: 'ApiSelect', | ||
| 97 | + label: '设备类型', | ||
| 98 | + colProps: { span: 8 }, | ||
| 99 | + rules: [{ message: '请选择设备类型', required: true }], | ||
| 100 | + componentProps: ({ formActionType }) => { | ||
| 101 | + const { setFieldsValue } = formActionType; | ||
| 102 | + return { | ||
| 103 | + api: findDictItemByCode, | ||
| 104 | + params: { | ||
| 105 | + dictCode: 'device_type', | ||
| 106 | + }, | ||
| 107 | + valueField: 'itemValue', | ||
| 108 | + labelField: 'itemText', | ||
| 109 | + placeholder: '请选择设备类型', | ||
| 110 | + onChange: (value: DeviceTypeEnum) => { | ||
| 111 | + setFieldsValue({ | ||
| 112 | + [DataSourceField.IS_GATEWAY_DEVICE]: value === DeviceTypeEnum.GATEWAY, | ||
| 113 | + [DataSourceField.DEVICE_PROFILE_ID]: null, | ||
| 114 | + [DataSourceField.DEVICE_ID]: null, | ||
| 115 | + [DataSourceField.ATTRIBUTE]: null, | ||
| 116 | + [DataSourceField.TRANSPORT_TYPE]: null, | ||
| 117 | + }); | ||
| 118 | + }, | ||
| 119 | + getPopupContainer: () => document.body, | ||
| 120 | + }; | ||
| 121 | + }, | ||
| 122 | + }, | ||
| 123 | + { | ||
| 124 | + field: DataSourceField.DEVICE_PROFILE_ID, | ||
| 125 | + component: 'ApiSelect', | ||
| 126 | + label: '产品', | ||
| 127 | + colProps: { span: 8 }, | ||
| 128 | + rules: [{ required: true, message: '产品为必填项' }], | ||
| 129 | + componentProps: ({ formActionType, formModel }) => { | ||
| 130 | + const { setFieldsValue } = formActionType; | ||
| 131 | + const deviceType = formModel[DataSourceField.DEVICE_TYPE]; | ||
| 132 | + return { | ||
| 133 | + api: async () => { | ||
| 134 | + if (!deviceType) return []; | ||
| 135 | + const list = await getDeviceProfile(deviceType); | ||
| 136 | + if (isUpdate) { | ||
| 137 | + const deviceProfileId = formModel[DataSourceField.DEVICE_PROFILE_ID]; | ||
| 138 | + const record = list.find((item) => item.id === deviceProfileId); | ||
| 139 | + setFieldsValue({ [DataSourceField.TRANSPORT_TYPE]: record?.transportType }); | ||
| 140 | + } | ||
| 141 | + return list; | ||
| 142 | + }, | ||
| 143 | + labelField: 'name', | ||
| 144 | + valueField: 'id', | ||
| 145 | + placeholder: '请选择产品', | ||
| 146 | + onChange: (_, option = {} as Record<'transportType', string>) => { | ||
| 147 | + setFieldsValue({ | ||
| 148 | + [DataSourceField.DEVICE_ID]: null, | ||
| 149 | + [DataSourceField.ATTRIBUTE]: null, | ||
| 150 | + [DataSourceField.TRANSPORT_TYPE]: option[DataSourceField.TRANSPORT_TYPE], | ||
| 151 | + }); | ||
| 152 | + }, | ||
| 153 | + getPopupContainer: () => document.body, | ||
| 154 | + }; | ||
| 155 | + }, | ||
| 156 | + }, | ||
| 157 | + { | ||
| 158 | + field: DataSourceField.ORIGINATION_ID, | ||
| 159 | + component: 'OrgTreeSelect', | ||
| 160 | + label: '组织', | ||
| 161 | + colProps: { span: 8 }, | ||
| 162 | + rules: [{ required: true, message: '组织为必填项' }], | ||
| 163 | + componentProps({ formActionType }) { | ||
| 164 | + const { setFieldsValue } = formActionType; | ||
| 165 | + return { | ||
| 166 | + placeholder: '请选择组织', | ||
| 167 | + onChange() { | ||
| 168 | + setFieldsValue({ | ||
| 169 | + [DataSourceField.DEVICE_ID]: null, | ||
| 170 | + }); | ||
| 171 | + }, | ||
| 172 | + showCreate: false, | ||
| 173 | + getPopupContainer: () => document.body, | ||
| 174 | + }; | ||
| 175 | + }, | ||
| 176 | + }, | ||
| 177 | + { | ||
| 178 | + field: DataSourceField.DEVICE_PROFILE_ID, | ||
| 179 | + component: 'Input', | ||
| 180 | + label: '', | ||
| 181 | + show: false, | ||
| 182 | + }, | ||
| 183 | + { | ||
| 184 | + field: DataSourceField.DEVICE_ID, | ||
| 185 | + component: 'ApiSelect', | ||
| 186 | + label: '设备', | ||
| 187 | + colProps: { span: 8 }, | ||
| 188 | + rules: [{ required: true, message: '设备名称为必填项' }], | ||
| 189 | + componentProps({ formModel, formActionType }) { | ||
| 190 | + const { setFieldsValue } = formActionType; | ||
| 191 | + const organizationId = formModel[DataSourceField.ORIGINATION_ID]; | ||
| 192 | + const deviceProfileId = formModel[DataSourceField.DEVICE_PROFILE_ID]; | ||
| 193 | + const deviceType = formModel[DataSourceField.DEVICE_TYPE]; | ||
| 194 | + | ||
| 195 | + return { | ||
| 196 | + api: async () => { | ||
| 197 | + if (organizationId) { | ||
| 198 | + try { | ||
| 199 | + const data = await getMeetTheConditionsDevice({ | ||
| 200 | + organizationId, | ||
| 201 | + deviceProfileId, | ||
| 202 | + deviceType, | ||
| 203 | + }); | ||
| 204 | + if (data) | ||
| 205 | + return data.map((item) => ({ | ||
| 206 | + ...item, | ||
| 207 | + label: item.alias || item.name, | ||
| 208 | + value: item.tbDeviceId, | ||
| 209 | + deviceType: item.deviceType, | ||
| 210 | + })); | ||
| 211 | + } catch (error) {} | ||
| 212 | + } | ||
| 213 | + return []; | ||
| 214 | + }, | ||
| 215 | + onChange(_value, record: MasterDeviceList) { | ||
| 216 | + setFieldsValue({ | ||
| 217 | + [DataSourceField.DEVICE_NAME]: record?.label, | ||
| 218 | + }); | ||
| 219 | + }, | ||
| 220 | + placeholder: '请选择设备', | ||
| 221 | + getPopupContainer: () => document.body, | ||
| 222 | + }; | ||
| 223 | + }, | ||
| 224 | + }, | ||
| 225 | + { | ||
| 226 | + field: DataSourceField.ATTRIBUTE, | ||
| 227 | + component: 'ApiSelect', | ||
| 228 | + label: '属性', | ||
| 229 | + colProps: { span: 8 }, | ||
| 230 | + rules: [{ required: true, message: '请选择属性' }], | ||
| 231 | + ifShow: ({ model }) => | ||
| 232 | + !(isTcpProfile(model[DataSourceField.TRANSPORT_TYPE]) && isControlComponent(category!)), | ||
| 233 | + componentProps({ formModel }) { | ||
| 234 | + const deviceProfileId = formModel[DataSourceField.DEVICE_PROFILE_ID]; | ||
| 235 | + return { | ||
| 236 | + api: async () => { | ||
| 237 | + try { | ||
| 238 | + if (deviceProfileId) { | ||
| 239 | + return await getDeviceAttribute({ | ||
| 240 | + deviceProfileId, | ||
| 241 | + dataType: isControlComponent(category!) ? DataTypeEnum.IS_BOOL : undefined, | ||
| 242 | + }); | ||
| 243 | + } | ||
| 244 | + } catch (error) {} | ||
| 245 | + return []; | ||
| 246 | + }, | ||
| 247 | + placeholder: '请选择属性', | ||
| 248 | + getPopupContainer: () => document.body, | ||
| 249 | + }; | ||
| 250 | + }, | ||
| 251 | + }, | ||
| 252 | + { | ||
| 253 | + field: DataSourceField.COMMAND_TYPE, | ||
| 254 | + component: 'ApiSelect', | ||
| 255 | + label: '命令类型', | ||
| 256 | + defaultValue: CommandTypeEnum.CUSTOM.toString(), | ||
| 257 | + rules: [{ required: true, message: '请选择命令类型' }], | ||
| 258 | + colProps: { span: 8 }, | ||
| 259 | + ifShow: ({ model }) => | ||
| 260 | + isControlComponent(category!) && isTcpProfile(model[DataSourceField.TRANSPORT_TYPE]), | ||
| 261 | + componentProps: ({ formActionType }) => { | ||
| 262 | + const { setFieldsValue } = formActionType; | ||
| 263 | + return { | ||
| 264 | + api: findDictItemByCode, | ||
| 265 | + params: { | ||
| 266 | + dictCode: 'custom_define', | ||
| 267 | + }, | ||
| 268 | + labelField: 'itemText', | ||
| 269 | + valueField: 'itemValue', | ||
| 270 | + placeholder: '请选择命令类型', | ||
| 271 | + onChange() { | ||
| 272 | + setFieldsValue({ [DataSourceField.COMMAND]: null, [DataSourceField.SERVICE]: null }); | ||
| 273 | + }, | ||
| 274 | + }; | ||
| 275 | + }, | ||
| 276 | + }, | ||
| 277 | + { | ||
| 278 | + field: DataSourceField.SERVICE, | ||
| 279 | + component: 'ApiSelect', | ||
| 280 | + label: '服务', | ||
| 281 | + colProps: { span: 8 }, | ||
| 282 | + rules: [{ required: true, message: '请选择服务' }], | ||
| 283 | + ifShow: ({ model }) => | ||
| 284 | + isControlComponent(category!) && | ||
| 285 | + model[DataSourceField.COMMAND_TYPE] === CommandTypeEnum.SERVICE.toString() && | ||
| 286 | + isTcpProfile(model[DataSourceField.TRANSPORT_TYPE]), | ||
| 287 | + componentProps({ formModel, formActionType }) { | ||
| 288 | + const { setFieldsValue } = formActionType; | ||
| 289 | + const deviceProfileId = formModel[DataSourceField.DEVICE_PROFILE_ID]; | ||
| 290 | + const transportType = formModel[DataSourceField.TRANSPORT_TYPE]; | ||
| 291 | + if (isUpdate && ![deviceProfileId, transportType].every(Boolean)) | ||
| 292 | + return { placeholder: '请选择服务', getPopupContainer: () => document.body }; | ||
| 293 | + return { | ||
| 294 | + api: async () => { | ||
| 295 | + try { | ||
| 296 | + if (deviceProfileId) { | ||
| 297 | + return await getDeviceService(deviceProfileId); | ||
| 298 | + } | ||
| 299 | + } catch (error) {} | ||
| 300 | + return []; | ||
| 301 | + }, | ||
| 302 | + placeholder: '请选择服务', | ||
| 303 | + getPopupContainer: () => document.body, | ||
| 304 | + onChange(value: string, options: ModelOfMatterParams) { | ||
| 305 | + const command = value ? (options.functionJson.inputData || [])[0].serviceCommand : null; | ||
| 306 | + setFieldsValue({ [DataSourceField.COMMAND]: command }); | ||
| 307 | + }, | ||
| 308 | + }; | ||
| 309 | + }, | ||
| 310 | + }, | ||
| 311 | + { | ||
| 312 | + field: DataSourceField.COMMAND, | ||
| 313 | + component: 'Input', | ||
| 314 | + label: '命令', | ||
| 315 | + colProps: { span: 8 }, | ||
| 316 | + rules: [{ required: true, message: '请输入下发命令' }], | ||
| 317 | + // 是控制组件 && 自定义命令 && 传输协议为TCP | ||
| 318 | + ifShow: ({ model }) => | ||
| 319 | + isControlComponent(category!) && | ||
| 320 | + model[DataSourceField.COMMAND_TYPE] === CommandTypeEnum.CUSTOM.toString() && | ||
| 321 | + model[DataSourceField.TRANSPORT_TYPE] && | ||
| 322 | + isTcpProfile(model[DataSourceField.TRANSPORT_TYPE]), | ||
| 323 | + componentProps: { | ||
| 324 | + placeholder: '请输入下发命令', | ||
| 325 | + }, | ||
| 326 | + }, | ||
| 327 | + { | ||
| 328 | + field: DataSourceField.DEVICE_RENAME, | ||
| 329 | + component: 'Input', | ||
| 330 | + label: '设备名', | ||
| 331 | + colProps: { span: 8 }, | ||
| 332 | + componentProps: { | ||
| 333 | + placeholder: '设备重命名', | ||
| 334 | + }, | ||
| 335 | + }, | ||
| 336 | + { | ||
| 337 | + field: DataSourceField.ATTRIBUTE_RENAME, | ||
| 338 | + component: 'Input', | ||
| 339 | + label: '属性', | ||
| 340 | + colProps: { span: 8 }, | ||
| 341 | + componentProps: { | ||
| 342 | + placeholder: '属性重命名', | ||
| 343 | + }, | ||
| 344 | + }, | ||
| 345 | + ]; | ||
| 346 | +}; |
src/views/visual/packages/enum.ts
0 → 100644
| 1 | +export enum ComponentConfigFieldEnum { | ||
| 2 | + FONT_COLOR = 'fontColor', | ||
| 3 | + UNIT = 'unit', | ||
| 4 | + ICON_COLOR = 'iconColor', | ||
| 5 | + ICON = 'icon', | ||
| 6 | + FIRST_PHASE_COLOR = 'firstPhaseColor', | ||
| 7 | + SECOND_PHASE_COLOR = 'secondPhaseColor', | ||
| 8 | + THIRD_PHASE_COLOR = 'thirdPhaseColor', | ||
| 9 | + FIRST_PHASE_VALUE = 'firstPhaseValue', | ||
| 10 | + SECOND_PHASE_VALUE = 'secondPhaseValue', | ||
| 11 | + THIRD_PHASE_VALUE = 'thirdPhaseValue', | ||
| 12 | + SHOW_DEVICE_NAME = 'showDeviceName', | ||
| 13 | + GRADIENT_INFO = 'gradientInfo', | ||
| 14 | + | ||
| 15 | + FLOWMETER_CONFIG = 'flowmeterConfig', | ||
| 16 | + WAVE_FIRST = 'waveFirst', | ||
| 17 | + WAVE_SECOND = 'waveSecond', | ||
| 18 | + WAVE_THIRD = 'waveThird', | ||
| 19 | + BACKGROUND_COLOR = 'backgroundColor', | ||
| 20 | +} |
| 1 | +import { onMounted, onUnmounted, ref } from 'vue'; | ||
| 2 | +import { useInjectScript } from '/@/hooks/web/useInjectScript'; | ||
| 3 | +import { BAI_DU_MAP_GL_LIB, BAI_DU_MAP_TRACK_ANIMATION } from '/@/utils/fnUtils'; | ||
| 4 | + | ||
| 5 | +export enum LoadStatusEnum { | ||
| 6 | + LOADING = 'LOADING', | ||
| 7 | + SUCCESS = 'SUCCESS', | ||
| 8 | + ERROR = 'ERROR', | ||
| 9 | +} | ||
| 10 | + | ||
| 11 | +export const useBaiduMapSDK = (completeCallback: Fn) => { | ||
| 12 | + const loadGLKey = 'loadBaiMapGL'; | ||
| 13 | + const loadGLLibKey = 'loadBaiMapGLLib'; | ||
| 14 | + const BaiduMapGLGlobalName = 'BMapGL'; | ||
| 15 | + const BaiduMapGLLibGlobalName = 'BMapGLLib'; | ||
| 16 | + const hasBMapGLFlag = Reflect.has(window, BaiduMapGLGlobalName); | ||
| 17 | + const hasBMapGLLibFlag = Reflect.has(window, BaiduMapGLLibGlobalName); | ||
| 18 | + const BMapGLRepeatLoadFlag = hasBMapGLFlag || Reflect.has(window, loadGLKey); | ||
| 19 | + const BMapGLLibRepeatLoadFlag = hasBMapGLLibFlag || Reflect.has(window, loadGLLibKey); | ||
| 20 | + | ||
| 21 | + if (!BMapGLRepeatLoadFlag) { | ||
| 22 | + Reflect.set(window, loadGLKey, true); | ||
| 23 | + const { toInject } = useInjectScript({ src: BAI_DU_MAP_GL_LIB }); | ||
| 24 | + toInject(); | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + if (!BMapGLLibRepeatLoadFlag) { | ||
| 28 | + Reflect.set(window, loadGLLibKey, true); | ||
| 29 | + const { toInject } = useInjectScript({ src: BAI_DU_MAP_TRACK_ANIMATION }); | ||
| 30 | + toInject(); | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + const waitFn = async () => { | ||
| 34 | + return new Promise((resolve) => { | ||
| 35 | + let interval: Nullable<NodeJS.Timer> = setInterval(() => { | ||
| 36 | + const hasBMapGLFlag = Reflect.has(window, BaiduMapGLGlobalName); | ||
| 37 | + const hasBMapGLLibFlag = Reflect.has(window, BaiduMapGLLibGlobalName); | ||
| 38 | + if (hasBMapGLFlag && hasBMapGLLibFlag) { | ||
| 39 | + resolve('success'); | ||
| 40 | + status.value = LoadStatusEnum.SUCCESS; | ||
| 41 | + loading.value = false; | ||
| 42 | + clearInterval(interval!); | ||
| 43 | + interval = null; | ||
| 44 | + clearTimeout(timeout!); | ||
| 45 | + timeout = null; | ||
| 46 | + } | ||
| 47 | + }, 300); | ||
| 48 | + | ||
| 49 | + let timeout: Nullable<NodeJS.Timeout> = setTimeout(() => { | ||
| 50 | + status.value = LoadStatusEnum.ERROR; | ||
| 51 | + clearTimeout(timeout!); | ||
| 52 | + timeout = null; | ||
| 53 | + resolve('error'); | ||
| 54 | + }, 10000); | ||
| 55 | + }); | ||
| 56 | + }; | ||
| 57 | + | ||
| 58 | + const loading = ref(true); | ||
| 59 | + | ||
| 60 | + const status = ref(LoadStatusEnum.LOADING); | ||
| 61 | + | ||
| 62 | + onMounted(async () => { | ||
| 63 | + await waitFn(); | ||
| 64 | + completeCallback?.(); | ||
| 65 | + loading.value = false; | ||
| 66 | + }); | ||
| 67 | + | ||
| 68 | + onUnmounted(() => { | ||
| 69 | + Reflect.deleteProperty(window, loadGLKey); | ||
| 70 | + Reflect.deleteProperty(window, loadGLLibKey); | ||
| 71 | + }); | ||
| 72 | + | ||
| 73 | + return { | ||
| 74 | + loading, | ||
| 75 | + status, | ||
| 76 | + }; | ||
| 77 | +}; |
| 1 | +import { CSSProperties, computed, unref, watch } from 'vue'; | ||
| 2 | +import { ComponentPropsConfigType } from '../index.type'; | ||
| 3 | +import { componentOptionsInitConfig } from '../publicConfig'; | ||
| 4 | + | ||
| 5 | +export const useComponentScale = (props: { config: ComponentPropsConfigType }, onScale?: Fn) => { | ||
| 6 | + const getRatio = computed(() => { | ||
| 7 | + try { | ||
| 8 | + const { option, attr } = props.config; | ||
| 9 | + const { w, h } = attr; | ||
| 10 | + const { widthPx, heightPx, itemWidthRatio, itemHeightRatio } = option; | ||
| 11 | + | ||
| 12 | + const currentW = (widthPx * itemWidthRatio) / 100; | ||
| 13 | + const currentH = (heightPx * itemHeightRatio) / 100; | ||
| 14 | + | ||
| 15 | + const widthScaleRatio = currentW / w; | ||
| 16 | + const heightScaleRatio = currentH / h; | ||
| 17 | + | ||
| 18 | + return Math.min(widthScaleRatio, heightScaleRatio); | ||
| 19 | + } catch (error) { | ||
| 20 | + return 1; | ||
| 21 | + } | ||
| 22 | + }); | ||
| 23 | + | ||
| 24 | + const getScaleRadio = computed(() => { | ||
| 25 | + try { | ||
| 26 | + const { persetOption = {} } = props.config; | ||
| 27 | + const { | ||
| 28 | + maxScale = componentOptionsInitConfig.maxScale, | ||
| 29 | + minScale = componentOptionsInitConfig.minScale, | ||
| 30 | + } = persetOption; | ||
| 31 | + | ||
| 32 | + let ratio = unref(getRatio); | ||
| 33 | + ratio = ratio > maxScale! ? maxScale! : ratio < minScale! ? minScale! : ratio; | ||
| 34 | + | ||
| 35 | + return ratio; | ||
| 36 | + } catch (error) { | ||
| 37 | + return 1; | ||
| 38 | + } | ||
| 39 | + }); | ||
| 40 | + | ||
| 41 | + const getScale = computed<CSSProperties>(() => { | ||
| 42 | + return { transform: `scale(${unref(getScaleRadio)})` }; | ||
| 43 | + }); | ||
| 44 | + | ||
| 45 | + onScale && | ||
| 46 | + watch(getRatio, () => { | ||
| 47 | + onScale?.(); | ||
| 48 | + }); | ||
| 49 | + | ||
| 50 | + return { getScale, getScaleRadio, getRatio }; | ||
| 51 | +}; |
| 1 | +import { createComponent } from '..'; | ||
| 2 | +import { transformComponentKey } from '../componentMap'; | ||
| 3 | +import { ConfigType, CreateComponentType } from '../index.type'; | ||
| 4 | + | ||
| 5 | +export const useGetComponentConfig = ( | ||
| 6 | + key: string, | ||
| 7 | + options: Recordable = {} | ||
| 8 | +): CreateComponentType => { | ||
| 9 | + try { | ||
| 10 | + const config = createComponent({ key: transformComponentKey(key) } as ConfigType, options); | ||
| 11 | + return config; | ||
| 12 | + } catch (error) { | ||
| 13 | + console.error(`can not get component config by component key (${key})`); | ||
| 14 | + return {} as CreateComponentType; | ||
| 15 | + } | ||
| 16 | +}; |
| @@ -12,14 +12,16 @@ export function useSendCommand() { | @@ -12,14 +12,16 @@ export function useSendCommand() { | ||
| 12 | createMessage.error('下发指令失败'); | 12 | createMessage.error('下发指令失败'); |
| 13 | return false; | 13 | return false; |
| 14 | }; | 14 | }; |
| 15 | + | ||
| 15 | const sendCommand = async (record: DataSource, value: any) => { | 16 | const sendCommand = async (record: DataSource, value: any) => { |
| 16 | - if (!record) return error(); | ||
| 17 | - const { customCommand, attribute } = record; | 17 | + if (!record) return false; |
| 18 | + const { customCommand, attribute } = record || {}; | ||
| 18 | 19 | ||
| 19 | const { deviceId } = record; | 20 | const { deviceId } = record; |
| 20 | - if (!deviceId) return error(); | ||
| 21 | - loading.value = true; | 21 | + if (!deviceId) return false; |
| 22 | + | ||
| 22 | try { | 23 | try { |
| 24 | + loading.value = true; | ||
| 23 | let params: string | Recordable = { | 25 | let params: string | Recordable = { |
| 24 | [attribute!]: Number(value), | 26 | [attribute!]: Number(value), |
| 25 | }; | 27 | }; |
| @@ -50,7 +52,7 @@ export function useSendCommand() { | @@ -50,7 +52,7 @@ export function useSendCommand() { | ||
| 50 | } | 52 | } |
| 51 | }; | 53 | }; |
| 52 | return { | 54 | return { |
| 53 | - sendCommand, | ||
| 54 | loading, | 55 | loading, |
| 56 | + sendCommand, | ||
| 55 | }; | 57 | }; |
| 56 | } | 58 | } |
src/views/visual/packages/hook/useSocket.ts
0 → 100644
| 1 | +import { Ref, computed, onUnmounted, unref, watch } from 'vue'; | ||
| 2 | +import { WidgetDataType } from '../../palette/hooks/useDataSource'; | ||
| 3 | +import { useWebSocket } from '@vueuse/core'; | ||
| 4 | +import { useGlobSetting } from '/@/hooks/setting'; | ||
| 5 | +import { isShareMode } from '/@/views/sys/share/hook'; | ||
| 6 | +import { getJwtToken, getShareJwtToken } from '/@/utils/auth'; | ||
| 7 | +import { isNullAndUnDef } from '/@/utils/is'; | ||
| 8 | +import { | ||
| 9 | + ComponentPropsConfigType, | ||
| 10 | + DataFetchUpdateFn, | ||
| 11 | + EntityTypeEnum, | ||
| 12 | + MultipleDataFetchUpdateFn, | ||
| 13 | + ReceiveMessageType, | ||
| 14 | + ScopeTypeEnum, | ||
| 15 | + SubscribeMessageItemType, | ||
| 16 | + SubscribeMessageType, | ||
| 17 | +} from '../index.type'; | ||
| 18 | +import { DataSource } from '../../palette/types'; | ||
| 19 | + | ||
| 20 | +interface DeviceGroupMapType { | ||
| 21 | + subscriptionId: number; | ||
| 22 | + attributes: Set<string>; | ||
| 23 | + subscriptionGroup: Record<'uuid' | 'attribute', string>[]; | ||
| 24 | +} | ||
| 25 | + | ||
| 26 | +interface ComponentUpdateFnMapValueType { | ||
| 27 | + fn: MultipleDataFetchUpdateFn; | ||
| 28 | + attributes: string[]; | ||
| 29 | +} | ||
| 30 | + | ||
| 31 | +const parseMessage = (text: string): ReceiveMessageType => { | ||
| 32 | + try { | ||
| 33 | + return JSON.parse(text); | ||
| 34 | + } catch (error) { | ||
| 35 | + return {} as ReceiveMessageType; | ||
| 36 | + } | ||
| 37 | +}; | ||
| 38 | + | ||
| 39 | +class Subscriber { | ||
| 40 | + subscribeId = 0; | ||
| 41 | + | ||
| 42 | + deviceGroupMap = new Map<string, DeviceGroupMapType>(); | ||
| 43 | + | ||
| 44 | + subscriptionMap = new Map<number, string>(); | ||
| 45 | + | ||
| 46 | + componentUpdateFnMap = new Map<string, DataFetchUpdateFn>(); | ||
| 47 | + | ||
| 48 | + componentGroupUpdateFnMap = new Map<string, ComponentUpdateFnMapValueType[]>(); | ||
| 49 | + | ||
| 50 | + getNextSubscribeId() { | ||
| 51 | + return this.subscribeId++; | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + clearSubscriber = () => { | ||
| 55 | + this.deviceGroupMap.clear(); | ||
| 56 | + this.subscriptionMap.clear(); | ||
| 57 | + this.componentUpdateFnMap.clear(); | ||
| 58 | + }; | ||
| 59 | + | ||
| 60 | + addSubscriber = (info: Record<'deviceId' | 'slaveDeviceId' | 'attribute' | 'uuid', string>) => { | ||
| 61 | + const { deviceId, attribute, uuid } = info; | ||
| 62 | + if (!this.deviceGroupMap.has(deviceId)) { | ||
| 63 | + this.deviceGroupMap.set(deviceId, { | ||
| 64 | + subscriptionId: this.getNextSubscribeId(), | ||
| 65 | + attributes: new Set(), | ||
| 66 | + subscriptionGroup: [], | ||
| 67 | + }); | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + const groupInfo = this.deviceGroupMap.get(deviceId); | ||
| 71 | + groupInfo?.attributes.add(attribute); | ||
| 72 | + groupInfo?.subscriptionGroup.push({ uuid, attribute }); | ||
| 73 | + | ||
| 74 | + this.subscriptionMap.set(groupInfo!.subscriptionId, deviceId); | ||
| 75 | + }; | ||
| 76 | + | ||
| 77 | + genBasicMessage(unsubscribe = false) { | ||
| 78 | + const message = Array.from(this.deviceGroupMap.entries()).map(([deviceId, value]) => { | ||
| 79 | + const { subscriptionId, attributes } = value; | ||
| 80 | + return { | ||
| 81 | + cmdId: subscriptionId, | ||
| 82 | + entityId: deviceId, | ||
| 83 | + keys: Array.from(attributes.values()).join(','), | ||
| 84 | + entityType: EntityTypeEnum.DEVICE, | ||
| 85 | + scope: ScopeTypeEnum.LATEST_TELEMERY, | ||
| 86 | + ...(unsubscribe ? { unsubscribe } : {}), | ||
| 87 | + } as SubscribeMessageItemType; | ||
| 88 | + }); | ||
| 89 | + return { tsSubCmds: message } as SubscribeMessageType; | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + genUnSubscribeMessage() { | ||
| 93 | + return this.genBasicMessage(true); | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + genSubscribeMessage() { | ||
| 97 | + return this.genBasicMessage(); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + getScopeMessage(message: ReceiveMessageType, attribute: string[]) { | ||
| 101 | + const data = attribute.reduce((prev, next) => { | ||
| 102 | + return { ...prev, [next]: (message.data || {})[next] || [[]] }; | ||
| 103 | + }, {} as ReceiveMessageType['data']); | ||
| 104 | + | ||
| 105 | + const latestValues = attribute.reduce((prev, next) => { | ||
| 106 | + return { ...prev, [next]: (message.latestValues || {})[next] || [[]] }; | ||
| 107 | + }, {} as ReceiveMessageType['data']); | ||
| 108 | + | ||
| 109 | + return { | ||
| 110 | + subscriptionId: message.subscriptionId, | ||
| 111 | + errorCode: message.errorCode, | ||
| 112 | + errorMsg: message.errorMsg, | ||
| 113 | + data, | ||
| 114 | + latestValues, | ||
| 115 | + } as ReceiveMessageType; | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + trackUpdate(uuid: string, fn: Fn) { | ||
| 119 | + if (!uuid || !fn) return; | ||
| 120 | + this.componentUpdateFnMap.set(uuid, fn); | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + trackUpdateGroup(deviceId: string, data: ComponentUpdateFnMapValueType) { | ||
| 124 | + if (!deviceId || !data) return; | ||
| 125 | + if (!this.componentGroupUpdateFnMap.has(deviceId)) | ||
| 126 | + this.componentGroupUpdateFnMap.set(deviceId, []); | ||
| 127 | + const temp = this.componentGroupUpdateFnMap.get(deviceId); | ||
| 128 | + temp?.push(data); | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + triggerUpdate(message: ReceiveMessageType) { | ||
| 132 | + const { subscriptionId } = message; | ||
| 133 | + if (isNullAndUnDef(subscriptionId)) return; | ||
| 134 | + const deviceId = this.subscriptionMap.get(subscriptionId); | ||
| 135 | + if (!deviceId) return; | ||
| 136 | + const deviceGroup = this.deviceGroupMap.get(deviceId); | ||
| 137 | + if (!deviceGroup) return; | ||
| 138 | + const { subscriptionGroup } = deviceGroup; | ||
| 139 | + | ||
| 140 | + const updateGroups = this.componentGroupUpdateFnMap.get(deviceId); | ||
| 141 | + | ||
| 142 | + if (updateGroups) { | ||
| 143 | + (updateGroups || []).forEach((item) => { | ||
| 144 | + const { attributes, fn } = item; | ||
| 145 | + try { | ||
| 146 | + if (!fn) return; | ||
| 147 | + fn?.(this.getScopeMessage(message, attributes), attributes); | ||
| 148 | + } catch (error) { | ||
| 149 | + console.error(`deviceId: ${deviceId}`); | ||
| 150 | + throw error; | ||
| 151 | + } | ||
| 152 | + }); | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + subscriptionGroup.forEach((item) => { | ||
| 156 | + const { attribute, uuid } = item; | ||
| 157 | + const updateFn = this.componentUpdateFnMap.get(uuid); | ||
| 158 | + try { | ||
| 159 | + if (!updateFn) return; | ||
| 160 | + updateFn?.(this.getScopeMessage(message, [attribute]), attribute); | ||
| 161 | + } catch (error) { | ||
| 162 | + console.error(`uuid: ${uuid}`); | ||
| 163 | + throw error; | ||
| 164 | + } | ||
| 165 | + }); | ||
| 166 | + } | ||
| 167 | +} | ||
| 168 | + | ||
| 169 | +const subscriber = new Subscriber(); | ||
| 170 | + | ||
| 171 | +export const useSocket = (dataSourceRef: Ref<WidgetDataType[]>) => { | ||
| 172 | + let initied = false; | ||
| 173 | + | ||
| 174 | + const { socketUrl } = useGlobSetting(); | ||
| 175 | + | ||
| 176 | + const token = isShareMode() ? getShareJwtToken() : getJwtToken(); | ||
| 177 | + | ||
| 178 | + const server = `${socketUrl}${token}`; | ||
| 179 | + | ||
| 180 | + const { send, data, close } = useWebSocket(server, { | ||
| 181 | + onMessage() { | ||
| 182 | + initied = true; | ||
| 183 | + try { | ||
| 184 | + const message = parseMessage(unref(data)); | ||
| 185 | + subscriber.triggerUpdate(message); | ||
| 186 | + } catch (error) { | ||
| 187 | + throw Error(error as string); | ||
| 188 | + } | ||
| 189 | + }, | ||
| 190 | + }); | ||
| 191 | + | ||
| 192 | + const initSubscribe = () => { | ||
| 193 | + subscriber.clearSubscriber(); | ||
| 194 | + unref(dataSourceRef).forEach((item) => { | ||
| 195 | + item.dataSource.forEach((temp) => { | ||
| 196 | + const { deviceId, slaveDeviceId, attribute, uuid } = temp; | ||
| 197 | + subscriber.addSubscriber({ deviceId, slaveDeviceId, attribute, uuid }); | ||
| 198 | + }); | ||
| 199 | + }); | ||
| 200 | + }; | ||
| 201 | + | ||
| 202 | + watch( | ||
| 203 | + () => dataSourceRef.value, | ||
| 204 | + (value) => { | ||
| 205 | + if (value.length) { | ||
| 206 | + if (initied) { | ||
| 207 | + const message = JSON.stringify(subscriber.genUnSubscribeMessage()); | ||
| 208 | + send(message); | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + initSubscribe(); | ||
| 212 | + | ||
| 213 | + const message = JSON.stringify(subscriber.genSubscribeMessage()); | ||
| 214 | + send(message); | ||
| 215 | + } | ||
| 216 | + } | ||
| 217 | + ); | ||
| 218 | + | ||
| 219 | + onUnmounted(() => { | ||
| 220 | + close(); | ||
| 221 | + }); | ||
| 222 | +}; | ||
| 223 | + | ||
| 224 | +export const useDataFetch = ( | ||
| 225 | + props: { config: ComponentPropsConfigType }, | ||
| 226 | + updateFn: DataFetchUpdateFn | ||
| 227 | +) => { | ||
| 228 | + const getBindAttribute = computed(() => { | ||
| 229 | + const { config } = props; | ||
| 230 | + const { option } = config as ComponentPropsConfigType<Recordable, DataSource>; | ||
| 231 | + return option.attribute; | ||
| 232 | + }); | ||
| 233 | + | ||
| 234 | + if (!unref(getBindAttribute)) return; | ||
| 235 | + | ||
| 236 | + const getUUID = computed(() => { | ||
| 237 | + return props.config.option.uuid; | ||
| 238 | + }); | ||
| 239 | + | ||
| 240 | + watch( | ||
| 241 | + () => getUUID, | ||
| 242 | + () => { | ||
| 243 | + subscriber.trackUpdate(unref(getUUID), updateFn); | ||
| 244 | + }, | ||
| 245 | + { | ||
| 246 | + immediate: true, | ||
| 247 | + } | ||
| 248 | + ); | ||
| 249 | + return { getUUID, getBindAttribute }; | ||
| 250 | +}; | ||
| 251 | + | ||
| 252 | +export const useMultipleDataFetch = ( | ||
| 253 | + props: { config: ComponentPropsConfigType }, | ||
| 254 | + updateFn: MultipleDataFetchUpdateFn | ||
| 255 | +) => { | ||
| 256 | + const getBindAttributes = computed(() => { | ||
| 257 | + const attributes = props.config.option.dataSource?.map((item) => item.attribute); | ||
| 258 | + return [...new Set(attributes)]; | ||
| 259 | + }); | ||
| 260 | + | ||
| 261 | + if (unref(getBindAttributes).length) return; | ||
| 262 | + | ||
| 263 | + const getDeviceId = computed(() => { | ||
| 264 | + return props.config.option.dataSource?.at(0)?.deviceId; | ||
| 265 | + }); | ||
| 266 | + | ||
| 267 | + watch( | ||
| 268 | + () => getDeviceId, | ||
| 269 | + () => { | ||
| 270 | + subscriber.trackUpdateGroup(unref(getDeviceId)!, { | ||
| 271 | + attributes: unref(getBindAttributes), | ||
| 272 | + fn: updateFn, | ||
| 273 | + }); | ||
| 274 | + }, | ||
| 275 | + { | ||
| 276 | + immediate: true, | ||
| 277 | + } | ||
| 278 | + ); | ||
| 279 | + | ||
| 280 | + return { getDeviceId, getBindAttributes }; | ||
| 281 | +}; |
src/views/visual/packages/index.ts
0 → 100644
| 1 | +import { ConfigType, FetchComFlagTypeEnum, PublicComponentOptions } from './index.type'; | ||
| 2 | + | ||
| 3 | +const configModules = import.meta.globEager('./components/**/config.vue'); | ||
| 4 | +const viewModules = import.meta.globEager('./components/**/index.vue'); | ||
| 5 | +const datasourceModules = import.meta.globEager('./components/**/datasource.vue'); | ||
| 6 | +const createInstaceModules = import.meta.globEager('./components/**/config.ts'); | ||
| 7 | + | ||
| 8 | +const findModule = (componentName: string, module: Recordable) => { | ||
| 9 | + for (const key in module) { | ||
| 10 | + const urlSplit = key.split('/'); | ||
| 11 | + if (urlSplit[urlSplit.length - 2] === componentName) { | ||
| 12 | + return module[key] as Record<'default', any>; | ||
| 13 | + } | ||
| 14 | + } | ||
| 15 | +}; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * @description 获取组件 | ||
| 19 | + * @param {string} componentName 组件名称 | ||
| 20 | + * @param {FetchComFlagTypeEnum} flag 标识 0展示组件 1配置组件 2数据源组件 | ||
| 21 | + */ | ||
| 22 | +export const fetchComponent = (componentName: string, flag: FetchComFlagTypeEnum) => { | ||
| 23 | + const module = | ||
| 24 | + flag === FetchComFlagTypeEnum.VIEW | ||
| 25 | + ? viewModules | ||
| 26 | + : flag === FetchComFlagTypeEnum.CONFIG | ||
| 27 | + ? configModules | ||
| 28 | + : datasourceModules; | ||
| 29 | + return findModule(componentName, module); | ||
| 30 | +}; | ||
| 31 | + | ||
| 32 | +export const fetchViewComponent = (configType: ConfigType) => { | ||
| 33 | + const { key } = configType; | ||
| 34 | + return fetchComponent(key, FetchComFlagTypeEnum.VIEW)?.default; | ||
| 35 | +}; | ||
| 36 | + | ||
| 37 | +export const fetchConfigComponent = (configType: ConfigType) => { | ||
| 38 | + const { key } = configType; | ||
| 39 | + return fetchComponent(key, FetchComFlagTypeEnum.CONFIG)?.default; | ||
| 40 | +}; | ||
| 41 | + | ||
| 42 | +export const fetchDatasourceComponent = (configType: ConfigType) => { | ||
| 43 | + const { key } = configType; | ||
| 44 | + return fetchComponent(key, FetchComFlagTypeEnum.DATASOURCE_FORM)?.default; | ||
| 45 | +}; | ||
| 46 | + | ||
| 47 | +/** | ||
| 48 | + * @description 获取目标组件配置信息 | ||
| 49 | + * @param targetData | ||
| 50 | + */ | ||
| 51 | +export const createComponent = ( | ||
| 52 | + targetData: ConfigType, | ||
| 53 | + options?: Partial<PublicComponentOptions> | ||
| 54 | +) => { | ||
| 55 | + const { key } = targetData; | ||
| 56 | + const module = findModule(key, createInstaceModules); | ||
| 57 | + const instance = module?.default; | ||
| 58 | + if (instance) { | ||
| 59 | + return new instance(options || {}); | ||
| 60 | + } | ||
| 61 | +}; |
src/views/visual/packages/index.type.ts
0 → 100644
| 1 | +import { ComponentInfoGradientInfoType, DataSource, FlowmeterConfigType } from '../palette/types'; | ||
| 2 | +import { ComponentConfigFieldEnum } from './enum'; | ||
| 3 | + | ||
| 4 | +/** | ||
| 5 | + * @description 获取组件 | ||
| 6 | + */ | ||
| 7 | +export enum FetchComFlagTypeEnum { | ||
| 8 | + VIEW, | ||
| 9 | + CONFIG, | ||
| 10 | + DATASOURCE_FORM, | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +/** | ||
| 14 | + * @description 包分类名称 | ||
| 15 | + */ | ||
| 16 | +export enum PackagesCategoryNameEnum { | ||
| 17 | + TEXT = '文本组件', | ||
| 18 | + INSTRUMENT = '仪表组件', | ||
| 19 | + PICTURE = '图片组件', | ||
| 20 | + CONTROL = '控制组件', | ||
| 21 | + MAP = '地图组件', | ||
| 22 | + FLOWMETER = '流量计', | ||
| 23 | + OTHER = '其他', | ||
| 24 | +} | ||
| 25 | + | ||
| 26 | +/** | ||
| 27 | + * @description 包分类枚举 | ||
| 28 | + */ | ||
| 29 | +export enum PackagesCategoryEnum { | ||
| 30 | + TEXT = 'TEXT', | ||
| 31 | + INSTRUMENT = 'INSTRUMENT', | ||
| 32 | + PICTURE = 'PICTURE', | ||
| 33 | + CONTROL = 'CONTROL', | ||
| 34 | + MAP = 'MAP', | ||
| 35 | + FLOWMETER = 'FLOWMETER', | ||
| 36 | + OTHER = 'FLOWMETER', | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +/** | ||
| 40 | + * @description 组件配置 | ||
| 41 | + */ | ||
| 42 | +export interface ConfigType { | ||
| 43 | + /** | ||
| 44 | + * @description | ||
| 45 | + */ | ||
| 46 | + key: string; | ||
| 47 | + | ||
| 48 | + /** | ||
| 49 | + * @description 组件key | ||
| 50 | + */ | ||
| 51 | + conKey: string; | ||
| 52 | + | ||
| 53 | + /** | ||
| 54 | + * @description 组件数据源组件key | ||
| 55 | + */ | ||
| 56 | + datasourceConKey: string; | ||
| 57 | + | ||
| 58 | + /** | ||
| 59 | + * @description 组件配置组件key | ||
| 60 | + */ | ||
| 61 | + configConKey: string; | ||
| 62 | + title: string; | ||
| 63 | + category?: string; | ||
| 64 | + categoryName?: string; | ||
| 65 | + package: string; | ||
| 66 | +} | ||
| 67 | + | ||
| 68 | +/** | ||
| 69 | + * @description 创建组件类型 | ||
| 70 | + */ | ||
| 71 | +export interface CreateComponentType<P = PublicPresetOptions, O = Partial<DataSource>> { | ||
| 72 | + key: string; | ||
| 73 | + componentConfig: ConfigType; | ||
| 74 | + option: PublicComponentOptions & O; | ||
| 75 | + persetOption?: P; | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +/** | ||
| 79 | + * @description 组件公共配置 | ||
| 80 | + */ | ||
| 81 | +export interface PublicConfigType { | ||
| 82 | + id: string; | ||
| 83 | + attr: { x: number; y: number; w: number; h: number }; | ||
| 84 | +} | ||
| 85 | + | ||
| 86 | +export interface PublicPresetOptions { | ||
| 87 | + maxScale?: number; | ||
| 88 | + minScale?: number; | ||
| 89 | + componetDesign?: boolean; | ||
| 90 | + multipleDataSourceComponent?: boolean; | ||
| 91 | + [ComponentConfigFieldEnum.FONT_COLOR]?: string; | ||
| 92 | + [ComponentConfigFieldEnum.UNIT]?: string; | ||
| 93 | + [ComponentConfigFieldEnum.ICON_COLOR]?: string; | ||
| 94 | + [ComponentConfigFieldEnum.ICON]?: string; | ||
| 95 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]?: boolean; | ||
| 96 | + [ComponentConfigFieldEnum.GRADIENT_INFO]?: ComponentInfoGradientInfoType[]; | ||
| 97 | + [ComponentConfigFieldEnum.FLOWMETER_CONFIG]?: FlowmeterConfigType; | ||
| 98 | + [key: string]: any; | ||
| 99 | +} | ||
| 100 | + | ||
| 101 | +export interface PublicComponentOptions { | ||
| 102 | + uuid: string; | ||
| 103 | + widthPx: number; | ||
| 104 | + heightPx: number; | ||
| 105 | + itemWidthRatio: number; | ||
| 106 | + itemHeightRatio: number; | ||
| 107 | + | ||
| 108 | + dataSource?: DataSource[]; | ||
| 109 | + [key: string]: any; | ||
| 110 | +} | ||
| 111 | + | ||
| 112 | +export interface ComponentPropsConfigType<P = PublicPresetOptions, O = DataSource> | ||
| 113 | + extends CreateComponentType<P, O>, | ||
| 114 | + PublicConfigType {} | ||
| 115 | + | ||
| 116 | +export interface PackagesType { | ||
| 117 | + [PackagesCategoryEnum.TEXT]: ConfigType[]; | ||
| 118 | + [PackagesCategoryEnum.PICTURE]: ConfigType[]; | ||
| 119 | + [PackagesCategoryEnum.INSTRUMENT]: ConfigType[]; | ||
| 120 | + [PackagesCategoryEnum.CONTROL]: ConfigType[]; | ||
| 121 | + [PackagesCategoryEnum.MAP]: ConfigType[]; | ||
| 122 | + [PackagesCategoryEnum.FLOWMETER]: ConfigType[]; | ||
| 123 | + [PackagesCategoryEnum.OTHER]: ConfigType[]; | ||
| 124 | +} | ||
| 125 | + | ||
| 126 | +export interface SubscribeMessageType { | ||
| 127 | + tsSubCmds: SubscribeMessageItemType[]; | ||
| 128 | +} | ||
| 129 | + | ||
| 130 | +export interface SubscribeMessageItemType { | ||
| 131 | + cmdId: number; | ||
| 132 | + entityId: string; | ||
| 133 | + entityType: string; | ||
| 134 | + keys?: string; | ||
| 135 | + scope?: string; | ||
| 136 | + unsubscribe?: boolean; | ||
| 137 | +} | ||
| 138 | + | ||
| 139 | +export interface ReceiveMessageType { | ||
| 140 | + subscriptionId: number; | ||
| 141 | + errorCode: number; | ||
| 142 | + errorMsg: Nullable<string>; | ||
| 143 | + data: Record<string, [number, string][]>; | ||
| 144 | + latestValues: Record<string, number>; | ||
| 145 | +} | ||
| 146 | + | ||
| 147 | +export enum EntityTypeEnum { | ||
| 148 | + DEVICE = 'DEVICE', | ||
| 149 | +} | ||
| 150 | + | ||
| 151 | +export enum ScopeTypeEnum { | ||
| 152 | + LATEST_TELEMERY = 'LATEST_TELEMERY', | ||
| 153 | +} | ||
| 154 | + | ||
| 155 | +export type DataFetchUpdateFn = (message: ReceiveMessageType, attr: string) => void; | ||
| 156 | + | ||
| 157 | +export type MultipleDataFetchUpdateFn = (message: ReceiveMessageType, attr: string[]) => void; | ||
| 158 | + | ||
| 159 | +// 旧 组件key | ||
| 160 | +// TEXT_COMPONENT_1 = 'text-component-1', | ||
| 161 | +// TEXT_COMPONENT_2 = 'text-component-2', | ||
| 162 | +// TEXT_COMPONENT_3 = 'text-component-3', | ||
| 163 | +// TEXT_COMPONENT_4 = 'text-component-4', | ||
| 164 | +// TEXT_COMPONENT_5 = 'text-component-5', | ||
| 165 | +// INSTRUMENT_COMPONENT_1 = 'instrument-component-1', | ||
| 166 | +// INSTRUMENT_COMPONENT_2 = 'instrument-component-2', | ||
| 167 | +// DIGITAL_DASHBOARD_COMPONENT = 'digital-dashboard-component', | ||
| 168 | +// PICTURE_COMPONENT_1 = 'picture-component-1', | ||
| 169 | +// CONTROL_COMPONENT_TOGGLE_SWITCH = 'control-component-toggle-switch', | ||
| 170 | +// CONTROL_COMPONENT_SWITCH_WITH_ICON = 'control-component-switch-with-icon', | ||
| 171 | +// CONTROL_COMPONENT_SLIDING_SWITCH = 'control-component-sliding-switch', | ||
| 172 | +// MAP_COMPONENT_TRACK_REAL = 'map-component-track-real', | ||
| 173 | +// MAP_COMPONENT_TRACK_HISTORY = 'map-component-track-history', |
src/views/visual/packages/package.ts
0 → 100644
| 1 | +import { ControlList } from './components/Control'; | ||
| 2 | +import { FlowmeterList } from './components/Flowmeter'; | ||
| 3 | +import { InstrumentList } from './components/Instrument'; | ||
| 4 | +import { MapList } from './components/Map'; | ||
| 5 | +import { PictureList } from './components/Picture'; | ||
| 6 | +import { TextList } from './components/Text'; | ||
| 7 | +import { PackagesCategoryEnum, PackagesType } from './index.type'; | ||
| 8 | + | ||
| 9 | +export const packageList: PackagesType = { | ||
| 10 | + [PackagesCategoryEnum.TEXT]: TextList, | ||
| 11 | + [PackagesCategoryEnum.INSTRUMENT]: InstrumentList, | ||
| 12 | + [PackagesCategoryEnum.PICTURE]: PictureList, | ||
| 13 | + [PackagesCategoryEnum.CONTROL]: ControlList, | ||
| 14 | + [PackagesCategoryEnum.MAP]: MapList, | ||
| 15 | + [PackagesCategoryEnum.FLOWMETER]: FlowmeterList, | ||
| 16 | +}; |
src/views/visual/packages/publicConfig.ts
0 → 100644
| 1 | +import { PublicConfigType, PublicPresetOptions } from './index.type'; | ||
| 2 | +import { buildUUID } from '/@/utils/uuid'; | ||
| 3 | + | ||
| 4 | +export const componentInitConfig = { x: 50, y: 50, w: 240, h: 200 }; | ||
| 5 | + | ||
| 6 | +export const componentOptionsInitConfig: PublicPresetOptions = { maxScale: 1, minScale: 0 }; | ||
| 7 | + | ||
| 8 | +export class PublicConfigClass implements PublicConfigType { | ||
| 9 | + public id = buildUUID(); | ||
| 10 | + public attr: Record<'x' | 'y' | 'w' | 'h', number> = { ...componentInitConfig }; | ||
| 11 | +} |
src/views/visual/packages/template/config.ts
0 → 100644
| 1 | +import cloneDeep from 'lodash-es/cloneDeep'; | ||
| 2 | +import { ComponentConfig } from '.'; | ||
| 3 | +import { | ||
| 4 | + ConfigType, | ||
| 5 | + CreateComponentType, | ||
| 6 | + PublicComponentOptions, | ||
| 7 | + PublicPresetOptions, | ||
| 8 | +} from '/@/views/visual/packages/index.type'; | ||
| 9 | +import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages/publicConfig'; | ||
| 10 | +import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 11 | + | ||
| 12 | +export const option: PublicPresetOptions = { | ||
| 13 | + [ComponentConfigFieldEnum.FONT_COLOR]: '#', | ||
| 14 | + [ComponentConfigFieldEnum.SHOW_DEVICE_NAME]: false, | ||
| 15 | +}; | ||
| 16 | + | ||
| 17 | +export default class Config extends PublicConfigClass implements CreateComponentType { | ||
| 18 | + public key: string = ComponentConfig.key; | ||
| 19 | + | ||
| 20 | + public attr = { ...componentInitConfig }; | ||
| 21 | + | ||
| 22 | + public componentConfig: ConfigType = cloneDeep(ComponentConfig); | ||
| 23 | + | ||
| 24 | + public persetOption = cloneDeep(option); | ||
| 25 | + | ||
| 26 | + public option: PublicComponentOptions; | ||
| 27 | + | ||
| 28 | + constructor(option: PublicComponentOptions) { | ||
| 29 | + super(); | ||
| 30 | + this.option = { ...option }; | ||
| 31 | + } | ||
| 32 | +} |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentConfigFieldEnum } from '/@/views/visual/packages/enum'; | ||
| 3 | + import { useForm, BasicForm } from '/@/components/Form'; | ||
| 4 | + import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 5 | + | ||
| 6 | + const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ | ||
| 7 | + schemas: [ | ||
| 8 | + { | ||
| 9 | + field: ComponentConfigFieldEnum.FONT_COLOR, | ||
| 10 | + label: '数值字体颜色', | ||
| 11 | + component: 'ColorPicker', | ||
| 12 | + changeEvent: 'update:value', | ||
| 13 | + componentProps: { | ||
| 14 | + defaultValue: '#FD7347', | ||
| 15 | + }, | ||
| 16 | + }, | ||
| 17 | + { | ||
| 18 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | ||
| 19 | + label: '显示设备名称', | ||
| 20 | + component: 'Checkbox', | ||
| 21 | + }, | ||
| 22 | + ], | ||
| 23 | + showActionButtonGroup: false, | ||
| 24 | + labelWidth: 120, | ||
| 25 | + baseColProps: { | ||
| 26 | + span: 12, | ||
| 27 | + }, | ||
| 28 | + }); | ||
| 29 | + | ||
| 30 | + const getFormValues = () => { | ||
| 31 | + return getFieldsValue(); | ||
| 32 | + }; | ||
| 33 | + | ||
| 34 | + const setFormValues = (data: Recordable) => { | ||
| 35 | + return setFieldsValue(data); | ||
| 36 | + }; | ||
| 37 | + | ||
| 38 | + defineExpose({ | ||
| 39 | + getFormValues, | ||
| 40 | + setFormValues, | ||
| 41 | + resetFormValues: resetFields, | ||
| 42 | + } as PublicFormInstaceType); | ||
| 43 | +</script> | ||
| 44 | + | ||
| 45 | +<template> | ||
| 46 | + <BasicForm @register="register" /> | ||
| 47 | +</template> |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { CreateComponentType } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
| 4 | + import { dataSourceSchema } from '/@/views/visual/board/detail/config/basicConfiguration'; | ||
| 5 | + import { | ||
| 6 | + PublicComponentValueType, | ||
| 7 | + PublicFormInstaceType, | ||
| 8 | + } from '/@/views/visual/dataSourceBindPanel/index.type'; | ||
| 9 | + | ||
| 10 | + const props = defineProps<{ | ||
| 11 | + values: PublicComponentValueType; | ||
| 12 | + componentConfig: CreateComponentType; | ||
| 13 | + }>(); | ||
| 14 | + | ||
| 15 | + const [register, { getFieldsValue, setFieldsValue, validate, resetFields }] = useForm({ | ||
| 16 | + labelWidth: 0, | ||
| 17 | + showActionButtonGroup: false, | ||
| 18 | + layout: 'horizontal', | ||
| 19 | + labelCol: { span: 0 }, | ||
| 20 | + schemas: dataSourceSchema(false, props.componentConfig.componentConfig.key), | ||
| 21 | + }); | ||
| 22 | + | ||
| 23 | + const getFormValues = () => { | ||
| 24 | + return getFieldsValue(); | ||
| 25 | + }; | ||
| 26 | + | ||
| 27 | + const setFormValues = (record: Recordable) => { | ||
| 28 | + return setFieldsValue(record); | ||
| 29 | + }; | ||
| 30 | + | ||
| 31 | + defineExpose({ | ||
| 32 | + getFormValues, | ||
| 33 | + setFormValues, | ||
| 34 | + validate, | ||
| 35 | + resetFormValues: resetFields, | ||
| 36 | + } as PublicFormInstaceType); | ||
| 37 | +</script> | ||
| 38 | + | ||
| 39 | +<template> | ||
| 40 | + <BasicForm @register="register" /> | ||
| 41 | +</template> |
src/views/visual/packages/template/index.ts
0 → 100644
| 1 | +import { useComponentKeys } from '/@/views/visual/packages/hook/useComponentKeys'; | ||
| 2 | +import { ConfigType, PackagesCategoryEnum } from '/@/views/visual/packages/index.type'; | ||
| 3 | + | ||
| 4 | +const componentKeys = useComponentKeys('componentKeys'); | ||
| 5 | + | ||
| 6 | +export const ComponentConfig: ConfigType = { | ||
| 7 | + ...componentKeys, | ||
| 8 | + title: '组件名', | ||
| 9 | + package: PackagesCategoryEnum.TEXT, | ||
| 10 | +}; |
src/views/visual/packages/template/index.vue
0 → 100644
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ComponentPropsConfigType, DataFetchUpdateFn } from '/@/views/visual/packages/index.type'; | ||
| 3 | + import { option } from './config'; | ||
| 4 | + import { useDataFetch } from '/@/views/visual/packages/hook/useSocket'; | ||
| 5 | + | ||
| 6 | + const props = defineProps<{ | ||
| 7 | + config: ComponentPropsConfigType<typeof option>; | ||
| 8 | + }>(); | ||
| 9 | + | ||
| 10 | + const updateFn: DataFetchUpdateFn = (_message) => {}; | ||
| 11 | + | ||
| 12 | + useDataFetch(props, updateFn); | ||
| 13 | +</script> | ||
| 14 | + | ||
| 15 | +<template> | ||
| 16 | + <main class="w-full h-full flex flex-col justify-center items-center"> </main> | ||
| 17 | +</template> |
| 1 | +import moment from 'moment'; | ||
| 2 | +import { Moment } from 'moment'; | ||
| 3 | +import { FormSchema } from '/@/components/Form'; | ||
| 4 | +import { ColEx } from '/@/components/Form/src/types'; | ||
| 5 | +import { useGridLayout } from '/@/hooks/component/useGridLayout'; | ||
| 6 | +import { | ||
| 7 | + getPacketIntervalByRange, | ||
| 8 | + getPacketIntervalByValue, | ||
| 9 | + intervalOption, | ||
| 10 | +} from '/@/views/device/localtion/cpns/TimePeriodForm/helper'; | ||
| 11 | +export enum QueryWay { | ||
| 12 | + LATEST = 'latest', | ||
| 13 | + TIME_PERIOD = 'timePeriod', | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +export enum SchemaFiled { | ||
| 17 | + DEVICE_ID = 'deviceId', | ||
| 18 | + WAY = 'way', | ||
| 19 | + TIME_PERIOD = 'timePeriod', | ||
| 20 | + KEYS = 'keys', | ||
| 21 | + DATE_RANGE = 'dataRange', | ||
| 22 | + START_TS = 'startTs', | ||
| 23 | + END_TS = 'endTs', | ||
| 24 | + INTERVAL = 'interval', | ||
| 25 | + LIMIT = 'limit', | ||
| 26 | + AGG = 'agg', | ||
| 27 | + ORDER_BY = 'orderBy', | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +export enum AggregateDataEnum { | ||
| 31 | + MIN = 'MIN', | ||
| 32 | + MAX = 'MAX', | ||
| 33 | + AVG = 'AVG', | ||
| 34 | + SUM = 'SUM', | ||
| 35 | + COUNT = 'COUNT', | ||
| 36 | + NONE = 'NONE', | ||
| 37 | +} | ||
| 38 | +export const formSchema = (): FormSchema[] => { | ||
| 39 | + return [ | ||
| 40 | + { | ||
| 41 | + field: SchemaFiled.DEVICE_ID, | ||
| 42 | + label: '设备名称', | ||
| 43 | + component: 'Select', | ||
| 44 | + rules: [{ required: true, message: '设备名称为必选项', type: 'string' }], | ||
| 45 | + componentProps({ formActionType }) { | ||
| 46 | + const { setFieldsValue } = formActionType; | ||
| 47 | + return { | ||
| 48 | + placeholder: '请选择设备', | ||
| 49 | + onChange() { | ||
| 50 | + setFieldsValue({ [SchemaFiled.KEYS]: null }); | ||
| 51 | + }, | ||
| 52 | + }; | ||
| 53 | + }, | ||
| 54 | + }, | ||
| 55 | + { | ||
| 56 | + field: SchemaFiled.WAY, | ||
| 57 | + label: '查询方式', | ||
| 58 | + component: 'RadioGroup', | ||
| 59 | + defaultValue: QueryWay.LATEST, | ||
| 60 | + componentProps({ formActionType }) { | ||
| 61 | + const { setFieldsValue } = formActionType; | ||
| 62 | + return { | ||
| 63 | + options: [ | ||
| 64 | + { label: '最后', value: QueryWay.LATEST }, | ||
| 65 | + { label: '时间段', value: QueryWay.TIME_PERIOD }, | ||
| 66 | + ], | ||
| 67 | + onChange(event: ChangeEvent) { | ||
| 68 | + (event.target as HTMLInputElement).value === QueryWay.LATEST | ||
| 69 | + ? setFieldsValue({ | ||
| 70 | + [SchemaFiled.DATE_RANGE]: [], | ||
| 71 | + [SchemaFiled.START_TS]: null, | ||
| 72 | + [SchemaFiled.END_TS]: null, | ||
| 73 | + }) | ||
| 74 | + : setFieldsValue({ [SchemaFiled.START_TS]: null }); | ||
| 75 | + }, | ||
| 76 | + getPopupContainer: () => document.body, | ||
| 77 | + }; | ||
| 78 | + }, | ||
| 79 | + }, | ||
| 80 | + { | ||
| 81 | + field: SchemaFiled.START_TS, | ||
| 82 | + label: '最后数据', | ||
| 83 | + component: 'Select', | ||
| 84 | + ifShow({ values }) { | ||
| 85 | + return values[SchemaFiled.WAY] === QueryWay.LATEST; | ||
| 86 | + }, | ||
| 87 | + componentProps({ formActionType }) { | ||
| 88 | + const { setFieldsValue } = formActionType; | ||
| 89 | + return { | ||
| 90 | + options: intervalOption, | ||
| 91 | + onChange() { | ||
| 92 | + setFieldsValue({ [SchemaFiled.INTERVAL]: null }); | ||
| 93 | + }, | ||
| 94 | + getPopupContainer: () => document.body, | ||
| 95 | + }; | ||
| 96 | + }, | ||
| 97 | + rules: [{ required: true, message: '最后数据为必选项', type: 'number' }], | ||
| 98 | + }, | ||
| 99 | + { | ||
| 100 | + field: SchemaFiled.DATE_RANGE, | ||
| 101 | + label: '时间段', | ||
| 102 | + component: 'RangePicker', | ||
| 103 | + ifShow({ values }) { | ||
| 104 | + return values[SchemaFiled.WAY] === QueryWay.TIME_PERIOD; | ||
| 105 | + }, | ||
| 106 | + rules: [{ required: true, message: '时间段为必选项' }], | ||
| 107 | + componentProps({ formActionType }) { | ||
| 108 | + const { setFieldsValue } = formActionType; | ||
| 109 | + let dates: Moment[] = []; | ||
| 110 | + return { | ||
| 111 | + showTime: { | ||
| 112 | + defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')], | ||
| 113 | + }, | ||
| 114 | + onCalendarChange(value: Moment[]) { | ||
| 115 | + dates = value; | ||
| 116 | + }, | ||
| 117 | + disabledDate(current: Moment) { | ||
| 118 | + if (!dates || dates.length === 0 || !current) { | ||
| 119 | + return false; | ||
| 120 | + } | ||
| 121 | + const diffDate = current.diff(dates[0], 'years', true); | ||
| 122 | + return Math.abs(diffDate) > 1; | ||
| 123 | + }, | ||
| 124 | + onChange() { | ||
| 125 | + dates = []; | ||
| 126 | + setFieldsValue({ [SchemaFiled.INTERVAL]: null }); | ||
| 127 | + }, | ||
| 128 | + getPopupContainer: () => document.body, | ||
| 129 | + }; | ||
| 130 | + }, | ||
| 131 | + colProps: useGridLayout(2, 2, 2, 2, 2, 2) as unknown as ColEx, | ||
| 132 | + }, | ||
| 133 | + { | ||
| 134 | + field: SchemaFiled.AGG, | ||
| 135 | + label: '数据聚合功能', | ||
| 136 | + component: 'Select', | ||
| 137 | + componentProps: { | ||
| 138 | + getPopupContainer: () => document.body, | ||
| 139 | + options: [ | ||
| 140 | + { label: '最小值', value: AggregateDataEnum.MIN }, | ||
| 141 | + { label: '最大值', value: AggregateDataEnum.MAX }, | ||
| 142 | + { label: '平均值', value: AggregateDataEnum.AVG }, | ||
| 143 | + { label: '求和', value: AggregateDataEnum.SUM }, | ||
| 144 | + { label: '计数', value: AggregateDataEnum.COUNT }, | ||
| 145 | + { label: '空', value: AggregateDataEnum.NONE }, | ||
| 146 | + ], | ||
| 147 | + }, | ||
| 148 | + }, | ||
| 149 | + { | ||
| 150 | + field: SchemaFiled.INTERVAL, | ||
| 151 | + label: '分组间隔', | ||
| 152 | + component: 'Select', | ||
| 153 | + dynamicRules: ({ model }) => { | ||
| 154 | + return [ | ||
| 155 | + { | ||
| 156 | + required: model[SchemaFiled.AGG] !== AggregateDataEnum.NONE, | ||
| 157 | + message: '分组间隔为必填项', | ||
| 158 | + type: 'number', | ||
| 159 | + }, | ||
| 160 | + ]; | ||
| 161 | + }, | ||
| 162 | + ifShow({ values }) { | ||
| 163 | + return values[SchemaFiled.AGG] !== AggregateDataEnum.NONE; | ||
| 164 | + }, | ||
| 165 | + componentProps({ formModel, formActionType }) { | ||
| 166 | + const options = | ||
| 167 | + formModel[SchemaFiled.WAY] === QueryWay.LATEST | ||
| 168 | + ? getPacketIntervalByValue(formModel[SchemaFiled.START_TS]) | ||
| 169 | + : getPacketIntervalByRange(formModel[SchemaFiled.DATE_RANGE]); | ||
| 170 | + if (formModel[SchemaFiled.AGG] !== AggregateDataEnum.NONE) { | ||
| 171 | + formActionType.setFieldsValue({ [SchemaFiled.LIMIT]: null }); | ||
| 172 | + } | ||
| 173 | + return { | ||
| 174 | + options, | ||
| 175 | + getPopupContainer: () => document.body, | ||
| 176 | + }; | ||
| 177 | + }, | ||
| 178 | + }, | ||
| 179 | + { | ||
| 180 | + field: SchemaFiled.LIMIT, | ||
| 181 | + label: '最大条数', | ||
| 182 | + component: 'InputNumber', | ||
| 183 | + ifShow({ values }) { | ||
| 184 | + return values[SchemaFiled.AGG] === AggregateDataEnum.NONE; | ||
| 185 | + }, | ||
| 186 | + helpMessage: ['根据查询条件,查出的数据条数不超过这个值'], | ||
| 187 | + componentProps() { | ||
| 188 | + return { | ||
| 189 | + max: 50000, | ||
| 190 | + min: 7, | ||
| 191 | + getPopupContainer: () => document.body, | ||
| 192 | + }; | ||
| 193 | + }, | ||
| 194 | + }, | ||
| 195 | + { | ||
| 196 | + field: SchemaFiled.KEYS, | ||
| 197 | + label: '设备属性', | ||
| 198 | + component: 'Select', | ||
| 199 | + componentProps: { | ||
| 200 | + getPopupContainer: () => document.body, | ||
| 201 | + }, | ||
| 202 | + }, | ||
| 203 | + ]; | ||
| 204 | +}; | ||
| 205 | + | ||
| 206 | +export function getHistorySearchParams(value: Recordable) { | ||
| 207 | + const { startTs, endTs, interval, agg, limit, way, keys, deviceId } = value; | ||
| 208 | + if (way === QueryWay.LATEST) { | ||
| 209 | + return { | ||
| 210 | + keys, | ||
| 211 | + entityId: deviceId, | ||
| 212 | + startTs: moment().subtract(startTs, 'ms').valueOf(), | ||
| 213 | + endTs: Date.now(), | ||
| 214 | + interval, | ||
| 215 | + agg, | ||
| 216 | + limit, | ||
| 217 | + }; | ||
| 218 | + } else { | ||
| 219 | + return { | ||
| 220 | + keys, | ||
| 221 | + entityId: deviceId, | ||
| 222 | + startTs: moment(startTs).valueOf(), | ||
| 223 | + endTs: moment(endTs).valueOf(), | ||
| 224 | + interval, | ||
| 225 | + agg, | ||
| 226 | + limit, | ||
| 227 | + }; | ||
| 228 | + } | ||
| 229 | +} |
| 1 | +export { default as HistoryTrendModal } from './index.vue'; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { computed, nextTick, Ref, ref, unref } from 'vue'; | ||
| 3 | + import { getDeviceHistoryInfo } from '/@/api/alarm/position'; | ||
| 4 | + import { Empty, Spin } from 'ant-design-vue'; | ||
| 5 | + import { useECharts } from '/@/hooks/web/useECharts'; | ||
| 6 | + import { AggregateDataEnum } from '/@/views/device/localtion/config.data'; | ||
| 7 | + import { useGridLayout } from '/@/hooks/component/useGridLayout'; | ||
| 8 | + import { ColEx } from '/@/components/Form/src/types'; | ||
| 9 | + import { useForm, BasicForm } from '/@/components/Form'; | ||
| 10 | + import { formSchema, SchemaFiled } from './config'; | ||
| 11 | + import BasicModal from '/@/components/Modal/src/BasicModal.vue'; | ||
| 12 | + import { useModalInner } from '/@/components/Modal'; | ||
| 13 | + import { getAllDeviceByOrg } from '/@/api/dataBoard'; | ||
| 14 | + import { useHistoryData } from '/@/views/device/list/hook/useHistoryData'; | ||
| 15 | + import { BasicTable, useTable } from '/@/components/Table'; | ||
| 16 | + import { formatToDateTime } from '/@/utils/dateUtil'; | ||
| 17 | + import { | ||
| 18 | + ModeSwitchButton, | ||
| 19 | + TABLE_CHART_MODE_LIST, | ||
| 20 | + EnumTableChartMode, | ||
| 21 | + } from '/@/components/Widget'; | ||
| 22 | + import { ModalParamsType } from '/#/utils'; | ||
| 23 | + import { WidgetDataType } from '../../hooks/useDataSource'; | ||
| 24 | + import { ExtraDataSource } from '../../types'; | ||
| 25 | + | ||
| 26 | + type DeviceOption = Record<'label' | 'value' | 'organizationId', string>; | ||
| 27 | + | ||
| 28 | + defineEmits(['register']); | ||
| 29 | + | ||
| 30 | + const mode = ref<EnumTableChartMode>(EnumTableChartMode.CHART); | ||
| 31 | + | ||
| 32 | + const chartRef = ref(); | ||
| 33 | + | ||
| 34 | + const loading = ref(false); | ||
| 35 | + | ||
| 36 | + const isNull = ref(false); | ||
| 37 | + | ||
| 38 | + const historyData = ref<{ ts: number; value: string; name: string }[]>([]); | ||
| 39 | + | ||
| 40 | + const { deviceAttrs, getDeviceKeys, getSearchParams, setChartOptions, getDeviceAttribute } = | ||
| 41 | + useHistoryData(); | ||
| 42 | + | ||
| 43 | + const { setOptions, destory } = useECharts(chartRef as Ref<HTMLDivElement>); | ||
| 44 | + | ||
| 45 | + function hasDeviceAttr() { | ||
| 46 | + return !!unref(deviceAttrs).length; | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + const [register, method] = useForm({ | ||
| 50 | + schemas: formSchema(), | ||
| 51 | + baseColProps: useGridLayout(2, 3, 4) as unknown as ColEx, | ||
| 52 | + rowProps: { | ||
| 53 | + gutter: 10, | ||
| 54 | + }, | ||
| 55 | + labelWidth: 120, | ||
| 56 | + fieldMapToTime: [ | ||
| 57 | + [SchemaFiled.DATE_RANGE, [SchemaFiled.START_TS, SchemaFiled.END_TS], 'YYYY-MM-DD HH:ss'], | ||
| 58 | + ], | ||
| 59 | + submitButtonOptions: { | ||
| 60 | + loading: loading as unknown as boolean, | ||
| 61 | + }, | ||
| 62 | + async submitFunc() { | ||
| 63 | + search(); | ||
| 64 | + }, | ||
| 65 | + }); | ||
| 66 | + | ||
| 67 | + const search = async () => { | ||
| 68 | + // 表单验证 | ||
| 69 | + await method.validate(); | ||
| 70 | + const value = method.getFieldsValue(); | ||
| 71 | + const searchParams = getSearchParams(value); | ||
| 72 | + if (!hasDeviceAttr()) return; | ||
| 73 | + // 发送请求 | ||
| 74 | + loading.value = true; | ||
| 75 | + const res = await getDeviceHistoryInfo(searchParams); | ||
| 76 | + historyData.value = getTableHistoryData(res); | ||
| 77 | + loading.value = false; | ||
| 78 | + // 判断数据对象是否为空 | ||
| 79 | + if (!Object.keys(res).length) { | ||
| 80 | + isNull.value = false; | ||
| 81 | + return; | ||
| 82 | + } else { | ||
| 83 | + isNull.value = true; | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + const selectedKeys = unref(deviceAttrs).find( | ||
| 87 | + (item) => item.identifier === value[SchemaFiled.KEYS] | ||
| 88 | + ); | ||
| 89 | + setOptions(setChartOptions(res, selectedKeys)); | ||
| 90 | + }; | ||
| 91 | + | ||
| 92 | + const getTableHistoryData = (record: Recordable<{ ts: number; value: string }[]>) => { | ||
| 93 | + const keys = Object.keys(record); | ||
| 94 | + const list = keys.reduce((prev, next) => { | ||
| 95 | + const list = record[next].map((item) => { | ||
| 96 | + return { | ||
| 97 | + ...item, | ||
| 98 | + name: next, | ||
| 99 | + }; | ||
| 100 | + }); | ||
| 101 | + return [...prev, ...list]; | ||
| 102 | + }, []); | ||
| 103 | + return list; | ||
| 104 | + }; | ||
| 105 | + | ||
| 106 | + const getIdentifierNameMapping = computed(() => { | ||
| 107 | + const mapping = {}; | ||
| 108 | + unref(deviceAttrs).forEach((item) => { | ||
| 109 | + const { identifier, name } = item; | ||
| 110 | + mapping[identifier] = name; | ||
| 111 | + }); | ||
| 112 | + return mapping; | ||
| 113 | + }); | ||
| 114 | + | ||
| 115 | + const [registerTable] = useTable({ | ||
| 116 | + showIndexColumn: false, | ||
| 117 | + showTableSetting: false, | ||
| 118 | + dataSource: historyData, | ||
| 119 | + maxHeight: 300, | ||
| 120 | + size: 'small', | ||
| 121 | + columns: [ | ||
| 122 | + { | ||
| 123 | + title: '属性', | ||
| 124 | + dataIndex: 'name', | ||
| 125 | + format: (value) => { | ||
| 126 | + return unref(getIdentifierNameMapping)[value]; | ||
| 127 | + }, | ||
| 128 | + }, | ||
| 129 | + { | ||
| 130 | + title: '值', | ||
| 131 | + dataIndex: 'value', | ||
| 132 | + }, | ||
| 133 | + { | ||
| 134 | + title: '更新时间', | ||
| 135 | + dataIndex: 'ts', | ||
| 136 | + format: (val) => { | ||
| 137 | + return formatToDateTime(val, 'YYYY-MM-DD HH:mm:ss'); | ||
| 138 | + }, | ||
| 139 | + }, | ||
| 140 | + ], | ||
| 141 | + }); | ||
| 142 | + | ||
| 143 | + const getDeviceDataKey = async (record: DeviceOption) => { | ||
| 144 | + const { organizationId, value } = record; | ||
| 145 | + try { | ||
| 146 | + const options = await getAllDeviceByOrg(organizationId); | ||
| 147 | + const record = options.find((item) => item.tbDeviceId === value); | ||
| 148 | + await getDeviceAttribute(record!); | ||
| 149 | + await nextTick(); | ||
| 150 | + method.updateSchema({ | ||
| 151 | + field: SchemaFiled.KEYS, | ||
| 152 | + componentProps: { | ||
| 153 | + options: unref(deviceAttrs).map((item) => ({ label: item.name, value: item.identifier })), | ||
| 154 | + }, | ||
| 155 | + }); | ||
| 156 | + } catch (error) { | ||
| 157 | + throw error; | ||
| 158 | + } | ||
| 159 | + }; | ||
| 160 | + | ||
| 161 | + const handleModalOpen = async () => { | ||
| 162 | + await nextTick(); | ||
| 163 | + | ||
| 164 | + method.setFieldsValue({ | ||
| 165 | + [SchemaFiled.START_TS]: 1 * 24 * 60 * 60 * 1000, | ||
| 166 | + [SchemaFiled.LIMIT]: 7, | ||
| 167 | + [SchemaFiled.AGG]: AggregateDataEnum.NONE, | ||
| 168 | + }); | ||
| 169 | + | ||
| 170 | + if (!hasDeviceAttr()) return; | ||
| 171 | + | ||
| 172 | + const { deviceId } = method.getFieldsValue(); | ||
| 173 | + | ||
| 174 | + const res = await getDeviceHistoryInfo({ | ||
| 175 | + entityId: deviceId, | ||
| 176 | + keys: unref(getDeviceKeys).join(), | ||
| 177 | + startTs: Date.now() - 1 * 24 * 60 * 60 * 1000, | ||
| 178 | + endTs: Date.now(), | ||
| 179 | + agg: AggregateDataEnum.NONE, | ||
| 180 | + limit: 7, | ||
| 181 | + }); | ||
| 182 | + historyData.value = getTableHistoryData(res); | ||
| 183 | + // 判断对象是否为空 | ||
| 184 | + if (!Object.keys(res).length) { | ||
| 185 | + isNull.value = false; | ||
| 186 | + return; | ||
| 187 | + } else { | ||
| 188 | + isNull.value = true; | ||
| 189 | + } | ||
| 190 | + setOptions(setChartOptions(res)); | ||
| 191 | + }; | ||
| 192 | + | ||
| 193 | + const generateDeviceOptions = (dataSource: ExtraDataSource[]) => { | ||
| 194 | + const record: { [key: string]: boolean } = {}; | ||
| 195 | + | ||
| 196 | + const options: DeviceOption[] = []; | ||
| 197 | + for (const item of dataSource) { | ||
| 198 | + const { deviceName, gatewayDevice, slaveDeviceId, organizationId } = item; | ||
| 199 | + let { deviceId } = item; | ||
| 200 | + if (gatewayDevice && slaveDeviceId) { | ||
| 201 | + deviceId = slaveDeviceId; | ||
| 202 | + } | ||
| 203 | + if (record[deviceId]) continue; | ||
| 204 | + options.push({ | ||
| 205 | + label: deviceName, | ||
| 206 | + value: deviceId, | ||
| 207 | + organizationId, | ||
| 208 | + }); | ||
| 209 | + record[deviceId] = true; | ||
| 210 | + } | ||
| 211 | + | ||
| 212 | + return options; | ||
| 213 | + }; | ||
| 214 | + | ||
| 215 | + const [registerModal] = useModalInner(async (params: ModalParamsType<WidgetDataType>) => { | ||
| 216 | + deviceAttrs.value = []; | ||
| 217 | + loading.value = false; | ||
| 218 | + const { record } = params; | ||
| 219 | + const options = generateDeviceOptions(record.dataSource); | ||
| 220 | + await nextTick(); | ||
| 221 | + method.updateSchema({ | ||
| 222 | + field: SchemaFiled.DEVICE_ID, | ||
| 223 | + componentProps({ formActionType }) { | ||
| 224 | + const { setFieldsValue } = formActionType; | ||
| 225 | + return { | ||
| 226 | + options, | ||
| 227 | + onChange(_, record: DeviceOption) { | ||
| 228 | + getDeviceDataKey(record); | ||
| 229 | + setFieldsValue({ [SchemaFiled.KEYS]: null }); | ||
| 230 | + }, | ||
| 231 | + }; | ||
| 232 | + }, | ||
| 233 | + }); | ||
| 234 | + | ||
| 235 | + if (options.length && options.at(0)?.value) { | ||
| 236 | + const record = options.at(0)!; | ||
| 237 | + await getDeviceDataKey(record); | ||
| 238 | + try { | ||
| 239 | + await method.setFieldsValue({ [SchemaFiled.DEVICE_ID]: record.value }); | ||
| 240 | + } catch (error) {} | ||
| 241 | + } | ||
| 242 | + | ||
| 243 | + await handleModalOpen(); | ||
| 244 | + }); | ||
| 245 | + | ||
| 246 | + const handleCancel = () => { | ||
| 247 | + destory(); | ||
| 248 | + }; | ||
| 249 | + | ||
| 250 | + const switchMode = (flag: EnumTableChartMode) => { | ||
| 251 | + mode.value = flag; | ||
| 252 | + }; | ||
| 253 | +</script> | ||
| 254 | + | ||
| 255 | +<template> | ||
| 256 | + <BasicModal | ||
| 257 | + @register="registerModal" | ||
| 258 | + @cancel="handleCancel" | ||
| 259 | + :destroy-on-close="true" | ||
| 260 | + :show-ok-btn="false" | ||
| 261 | + cancel-text="关闭" | ||
| 262 | + width="70%" | ||
| 263 | + title="历史趋势" | ||
| 264 | + > | ||
| 265 | + <section | ||
| 266 | + class="flex flex-col p-4 h-full w-full min-w-7/10" | ||
| 267 | + style="color: #f0f2f5; background-color: #f0f2f5" | ||
| 268 | + > | ||
| 269 | + <section class="bg-white my-3 p-2"> | ||
| 270 | + <BasicForm @register="register" /> | ||
| 271 | + </section> | ||
| 272 | + <section class="bg-white p-3" style="min-height: 350px"> | ||
| 273 | + <Spin :spinning="loading" :absolute="true"> | ||
| 274 | + <div | ||
| 275 | + v-show="mode === EnumTableChartMode.CHART" | ||
| 276 | + class="flex h-70px items-center justify-end p-2" | ||
| 277 | + > | ||
| 278 | + <ModeSwitchButton | ||
| 279 | + v-model:value="mode" | ||
| 280 | + :mode="TABLE_CHART_MODE_LIST" | ||
| 281 | + @change="switchMode" | ||
| 282 | + /> | ||
| 283 | + </div> | ||
| 284 | + | ||
| 285 | + <div | ||
| 286 | + v-show="isNull && mode === EnumTableChartMode.CHART" | ||
| 287 | + ref="chartRef" | ||
| 288 | + :style="{ height: '350px', width: '100%' }" | ||
| 289 | + > | ||
| 290 | + </div> | ||
| 291 | + <Empty | ||
| 292 | + v-if="mode === EnumTableChartMode.CHART" | ||
| 293 | + class="h-350px flex flex-col justify-center items-center" | ||
| 294 | + description="暂无数据,请选择设备查询" | ||
| 295 | + v-show="!isNull" | ||
| 296 | + /> | ||
| 297 | + | ||
| 298 | + <BasicTable v-show="mode === EnumTableChartMode.TABLE" @register="registerTable"> | ||
| 299 | + <template #toolbar> | ||
| 300 | + <div class="flex h-70px items-center justify-end p-2"> | ||
| 301 | + <ModeSwitchButton | ||
| 302 | + v-model:value="mode" | ||
| 303 | + :mode="TABLE_CHART_MODE_LIST" | ||
| 304 | + @change="switchMode" | ||
| 305 | + /> | ||
| 306 | + </div> | ||
| 307 | + </template> | ||
| 308 | + </BasicTable> | ||
| 309 | + </Spin> | ||
| 310 | + </section> | ||
| 311 | + </section> | ||
| 312 | + </BasicModal> | ||
| 313 | +</template> | ||
| 314 | + | ||
| 315 | +<style scoped></style> |
| 1 | +export { default as PageHeader } from './index.vue'; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { PageHeader } from 'ant-design-vue'; | ||
| 3 | + import { computed, unref } from 'vue'; | ||
| 4 | + import { useRoute, useRouter } from 'vue-router'; | ||
| 5 | + import { decode } from '../..'; | ||
| 6 | + import { RollbackOutlined } from '@ant-design/icons-vue'; | ||
| 7 | + | ||
| 8 | + defineProps<{ widgetNumber: number }>(); | ||
| 9 | + | ||
| 10 | + const ROUTE = useRoute(); | ||
| 11 | + const ROUTER = useRouter(); | ||
| 12 | + const getIsSharePage = computed(() => { | ||
| 13 | + return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId'); | ||
| 14 | + }); | ||
| 15 | + | ||
| 16 | + const getDataBoardName = computed(() => { | ||
| 17 | + return decode((ROUTE.params as { boardName: string }).boardName || ''); | ||
| 18 | + }); | ||
| 19 | + | ||
| 20 | + const handleBack = () => { | ||
| 21 | + if (unref(getIsSharePage)) return; | ||
| 22 | + ROUTER.go(-1); | ||
| 23 | + }; | ||
| 24 | + | ||
| 25 | + // const handleOpenCreatePanel = () => {}; | ||
| 26 | +</script> | ||
| 27 | + | ||
| 28 | +<template> | ||
| 29 | + <PageHeader v-if="!getIsSharePage" class="pagerheader !px-5 !pt-5 !pb-0 !dark:bg-dark-700"> | ||
| 30 | + <template #title> | ||
| 31 | + <div class="flex items-center h-18 px-4"> | ||
| 32 | + <RollbackOutlined | ||
| 33 | + v-if="!getIsSharePage" | ||
| 34 | + class="mr-3 cursor-pointer text-2xl" | ||
| 35 | + @click="handleBack" | ||
| 36 | + /> | ||
| 37 | + <span class="text-lg text-gray-700 dark:text-light-50">{{ getDataBoardName }}</span> | ||
| 38 | + </div> | ||
| 39 | + </template> | ||
| 40 | + <template #extra> | ||
| 41 | + <div class="w-full h-full flex justify-center items-center px-4"> | ||
| 42 | + <slot></slot> | ||
| 43 | + </div> | ||
| 44 | + </template> | ||
| 45 | + <div> | ||
| 46 | + <span class="mr-3 text-sm text-gray-500">已创建组件:</span> | ||
| 47 | + <span class="text-blue-500"> {{ widgetNumber }}个</span> | ||
| 48 | + </div> | ||
| 49 | + </PageHeader> | ||
| 50 | +</template> | ||
| 51 | + | ||
| 52 | +<style lang="less" scoped> | ||
| 53 | + .pagerheader { | ||
| 54 | + :deep(.ant-page-header-heading) { | ||
| 55 | + @apply bg-light-50 dark:bg-dark-900; | ||
| 56 | + } | ||
| 57 | + } | ||
| 58 | +</style> |
| 1 | +export { default as WidgetDistribute } from './index.vue'; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { computed } from 'vue'; | ||
| 3 | + import { WidgetDataType } from '../../hooks/useDataSource'; | ||
| 4 | + import { fetchViewComponent } from '../../../packages'; | ||
| 5 | + import { ConfigType } from '/@/views/visual/packages/index.type'; | ||
| 6 | + import { ExtraDataSource } from '../../types'; | ||
| 7 | + import { CSSProperties } from 'vue'; | ||
| 8 | + import { toRaw } from 'vue'; | ||
| 9 | + import { Spin } from 'ant-design-vue'; | ||
| 10 | + import { componentMap, transformComponentKey } from '../../../packages/componentMap'; | ||
| 11 | + import { onMounted } from 'vue'; | ||
| 12 | + import { registerComponent } from '../../../packages/componentMap'; | ||
| 13 | + import { useGetComponentConfig } from '../../../packages/hook/useGetComponetConfig'; | ||
| 14 | + import { unref } from 'vue'; | ||
| 15 | + import { isBoolean } from '/@/utils/is'; | ||
| 16 | + | ||
| 17 | + const props = defineProps<{ | ||
| 18 | + sourceInfo: WidgetDataType; | ||
| 19 | + }>(); | ||
| 20 | + | ||
| 21 | + const getLayout = computed(() => { | ||
| 22 | + const { itemWidthRatio, itemHeightRatio } = props.sourceInfo || {}; | ||
| 23 | + return { width: `${itemWidthRatio}%`, height: `${itemHeightRatio}%` } as CSSProperties; | ||
| 24 | + }); | ||
| 25 | + | ||
| 26 | + const getCanRenderFlag = computed(() => { | ||
| 27 | + const { itemHeightRatio, itemWidthRatio } = props.sourceInfo; | ||
| 28 | + return [itemHeightRatio, itemWidthRatio].every(Boolean); | ||
| 29 | + }); | ||
| 30 | + | ||
| 31 | + const getComponentConfig = computed(() => { | ||
| 32 | + const { widthPx, heightPx, itemWidthRatio, itemHeightRatio, frontId } = props.sourceInfo; | ||
| 33 | + | ||
| 34 | + return useGetComponentConfig(frontId, { | ||
| 35 | + widthPx, | ||
| 36 | + heightPx, | ||
| 37 | + itemWidthRatio, | ||
| 38 | + itemHeightRatio, | ||
| 39 | + }); | ||
| 40 | + }); | ||
| 41 | + | ||
| 42 | + const getBindValue = computed(() => { | ||
| 43 | + return (options: ExtraDataSource) => { | ||
| 44 | + const raw = toRaw(options); | ||
| 45 | + const config = unref(getComponentConfig); | ||
| 46 | + return { | ||
| 47 | + config: { | ||
| 48 | + ...config, | ||
| 49 | + option: { ...config.option, ...raw }, | ||
| 50 | + }, | ||
| 51 | + }; | ||
| 52 | + }; | ||
| 53 | + }); | ||
| 54 | + | ||
| 55 | + const getIsMultipleDataSourceComponent = computed(() => { | ||
| 56 | + const { persetOption } = unref(getComponentConfig); | ||
| 57 | + const { multipleDataSourceComponent } = persetOption || {}; | ||
| 58 | + return isBoolean(multipleDataSourceComponent) ? multipleDataSourceComponent : false; | ||
| 59 | + }); | ||
| 60 | + | ||
| 61 | + const getBindMultipleDataSourceValue = computed(() => { | ||
| 62 | + const config = unref(getComponentConfig); | ||
| 63 | + const { dataSource } = props.sourceInfo; | ||
| 64 | + return { | ||
| 65 | + config: { | ||
| 66 | + ...config, | ||
| 67 | + option: { ...config.option, dataSource: toRaw(dataSource) }, | ||
| 68 | + }, | ||
| 69 | + }; | ||
| 70 | + }); | ||
| 71 | + | ||
| 72 | + onMounted(() => { | ||
| 73 | + const key = transformComponentKey(props.sourceInfo.frontId); | ||
| 74 | + const component = fetchViewComponent({ key } as ConfigType); | ||
| 75 | + registerComponent(key, component); | ||
| 76 | + }); | ||
| 77 | +</script> | ||
| 78 | + | ||
| 79 | +<template> | ||
| 80 | + <Spin :spinning="!getCanRenderFlag" wrapperClassName="widget-distribute-spin"> | ||
| 81 | + <!-- 多数据源组件 --> | ||
| 82 | + <template v-if="getCanRenderFlag"> | ||
| 83 | + <template v-if="getIsMultipleDataSourceComponent"> | ||
| 84 | + <component | ||
| 85 | + :is="componentMap.get(transformComponentKey(props.sourceInfo.frontId))" | ||
| 86 | + class="w-full h-full" | ||
| 87 | + v-bind="getBindMultipleDataSourceValue" | ||
| 88 | + /> | ||
| 89 | + </template> | ||
| 90 | + | ||
| 91 | + <!-- 单一数据源组件 --> | ||
| 92 | + <template v-if="!getIsMultipleDataSourceComponent"> | ||
| 93 | + <component | ||
| 94 | + v-for="item in props.sourceInfo.dataSource" | ||
| 95 | + :key="item.uuid" | ||
| 96 | + :is="componentMap.get(transformComponentKey(props.sourceInfo.frontId))" | ||
| 97 | + :style="getLayout" | ||
| 98 | + v-bind="getBindValue(item)" | ||
| 99 | + /> | ||
| 100 | + </template> | ||
| 101 | + </template> | ||
| 102 | + </Spin> | ||
| 103 | +</template> | ||
| 104 | + | ||
| 105 | +<style lang="less" scoped> | ||
| 106 | + .widget-distribute-spin { | ||
| 107 | + @apply w-full h-full overflow-hidden; | ||
| 108 | + | ||
| 109 | + :deep(.ant-spin-container) { | ||
| 110 | + @apply w-full h-full flex flex-wrap; | ||
| 111 | + } | ||
| 112 | + } | ||
| 113 | +</style> |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ref } from 'vue'; | ||
| 3 | + import { useShare } from '../../hooks/useShare'; | ||
| 4 | + import AuthDropDown, { AuthDropMenuList } from '/@/components/Widget/AuthDropDown.vue'; | ||
| 5 | + import { useRole } from '/@/hooks/business/useRole'; | ||
| 6 | + import { DataActionModeEnum } from '/@/enums/toolEnum'; | ||
| 7 | + import { VisualComponentPermission } from '../..'; | ||
| 8 | + import { Tooltip } from 'ant-design-vue'; | ||
| 9 | + import { MoreOutlined, AreaChartOutlined } from '@ant-design/icons-vue'; | ||
| 10 | + import { WidgetDataType } from '../../hooks/useDataSource'; | ||
| 11 | + import { useMessage } from '/@/hooks/web/useMessage'; | ||
| 12 | + import { addDataComponent, deleteDataComponent } from '/@/api/dataBoard'; | ||
| 13 | + import { useBoardId } from '../../hooks/useBoardId'; | ||
| 14 | + import { unref, toRaw } from 'vue'; | ||
| 15 | + import { ApiDataBoardDataType } from '../../types'; | ||
| 16 | + import { useCalcNewWidgetPosition } from '../../hooks/useCalcNewWidgetPosition'; | ||
| 17 | + import { Layout } from 'vue3-grid-layout'; | ||
| 18 | + import { DataComponentRecord } from '/@/api/dataBoard/model'; | ||
| 19 | + | ||
| 20 | + const props = defineProps<{ | ||
| 21 | + sourceInfo: WidgetDataType; | ||
| 22 | + rawDataSource: ApiDataBoardDataType; | ||
| 23 | + }>(); | ||
| 24 | + | ||
| 25 | + const emit = defineEmits<{ | ||
| 26 | + (event: 'ok'): void; | ||
| 27 | + (event: 'update', data: WidgetDataType): void; | ||
| 28 | + (event: 'openTrend', data: WidgetDataType): void; | ||
| 29 | + }>(); | ||
| 30 | + | ||
| 31 | + const { isCustomerUser } = useRole(); | ||
| 32 | + | ||
| 33 | + const { getIsSharePage } = useShare(); | ||
| 34 | + | ||
| 35 | + const { createMessage } = useMessage(); | ||
| 36 | + | ||
| 37 | + const { boardId } = useBoardId(); | ||
| 38 | + | ||
| 39 | + const dropMenuList = ref<AuthDropMenuList[]>([ | ||
| 40 | + { | ||
| 41 | + text: '编辑组件', | ||
| 42 | + event: DataActionModeEnum.UPDATE, | ||
| 43 | + icon: 'ant-design:edit-outlined', | ||
| 44 | + auth: VisualComponentPermission.UPDATE, | ||
| 45 | + onClick: handleUpdate, | ||
| 46 | + }, | ||
| 47 | + { | ||
| 48 | + text: '复制组件', | ||
| 49 | + event: DataActionModeEnum.COPY, | ||
| 50 | + icon: 'ant-design:copy-outlined', | ||
| 51 | + auth: VisualComponentPermission.COPY, | ||
| 52 | + onClick: handleCopy, | ||
| 53 | + }, | ||
| 54 | + { | ||
| 55 | + text: '删除组件', | ||
| 56 | + event: DataActionModeEnum.DELETE, | ||
| 57 | + icon: 'ant-design:delete-outlined', | ||
| 58 | + auth: VisualComponentPermission.DELETE, | ||
| 59 | + onClick: handleDelete, | ||
| 60 | + }, | ||
| 61 | + ]); | ||
| 62 | + | ||
| 63 | + function handleUpdate() { | ||
| 64 | + emit('update', toRaw(props.sourceInfo)); | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + async function handleDelete() { | ||
| 68 | + try { | ||
| 69 | + await deleteDataComponent({ dataBoardId: unref(boardId), ids: [props.sourceInfo.id] }); | ||
| 70 | + createMessage.success('删除成功~'); | ||
| 71 | + emit('ok'); | ||
| 72 | + } catch (error) {} | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + async function handleCopy() { | ||
| 76 | + const id = props.sourceInfo.id; | ||
| 77 | + const copyRecord = props.rawDataSource.componentData.find((item) => item.id === id); | ||
| 78 | + const copyLayout = props.rawDataSource.componentLayout.find((item) => item.id === id); | ||
| 79 | + | ||
| 80 | + const raw = toRaw(copyRecord) as unknown as DataComponentRecord; | ||
| 81 | + | ||
| 82 | + const layout = useCalcNewWidgetPosition( | ||
| 83 | + props.rawDataSource.componentLayout as unknown as Layout[], | ||
| 84 | + { width: copyLayout!.w, height: copyLayout!.h } | ||
| 85 | + ); | ||
| 86 | + | ||
| 87 | + try { | ||
| 88 | + await addDataComponent({ | ||
| 89 | + boardId: unref(boardId), | ||
| 90 | + record: { | ||
| 91 | + frontId: raw.frontId, | ||
| 92 | + dataSource: raw.dataSource, | ||
| 93 | + name: raw.name, | ||
| 94 | + remark: raw.remark, | ||
| 95 | + layout: layout as Layout, | ||
| 96 | + }, | ||
| 97 | + }); | ||
| 98 | + createMessage.success('复制成功~'); | ||
| 99 | + emit('ok'); | ||
| 100 | + } catch (error) { | ||
| 101 | + throw error; | ||
| 102 | + } | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + const handleOpenTrendModal = () => { | ||
| 106 | + emit('openTrend', toRaw(props.sourceInfo)); | ||
| 107 | + }; | ||
| 108 | +</script> | ||
| 109 | + | ||
| 110 | +<template> | ||
| 111 | + <section class="p-5 flex flex-col w-full"> | ||
| 112 | + <main class="flex w-full h-full h-7"> | ||
| 113 | + <Tooltip :title="sourceInfo.name"> | ||
| 114 | + <div class="flex-1 w-full h-full flex text-lg justify-center font-semibold"> | ||
| 115 | + {{ sourceInfo.name }} | ||
| 116 | + </div> | ||
| 117 | + </Tooltip> | ||
| 118 | + | ||
| 119 | + <div v-if="!getIsSharePage" class="flex items-center w-16 justify-evenly"> | ||
| 120 | + <Tooltip v-if="!isCustomerUser" title="趋势"> | ||
| 121 | + <AreaChartOutlined class="text-lg" @click="handleOpenTrendModal" /> | ||
| 122 | + </Tooltip> | ||
| 123 | + <AuthDropDown | ||
| 124 | + v-if="!isCustomerUser && dropMenuList.length" | ||
| 125 | + :drop-menu-list="dropMenuList" | ||
| 126 | + :trigger="['click']" | ||
| 127 | + > | ||
| 128 | + <Tooltip title="更多"> | ||
| 129 | + <MoreOutlined class="transform rotate-90 cursor-pointer text-lg" /> | ||
| 130 | + </Tooltip> | ||
| 131 | + </AuthDropDown> | ||
| 132 | + </div> | ||
| 133 | + </main> | ||
| 134 | + </section> | ||
| 135 | +</template> |
| 1 | +<script lang="ts" setup></script> | ||
| 2 | + | ||
| 3 | +<template> | ||
| 4 | + <section class="widget-wrapper !dark:bg-dark-900 flex flex-col bg-light-50 w-full h-full"> | ||
| 5 | + <!-- Header --> | ||
| 6 | + <slot name="header"></slot> | ||
| 7 | + | ||
| 8 | + <!-- Container --> | ||
| 9 | + <slot></slot> | ||
| 10 | + | ||
| 11 | + <!-- Footer --> | ||
| 12 | + <slot name="footer"></slot> | ||
| 13 | + </section> | ||
| 14 | +</template> |
src/views/visual/palette/hooks/useBoardId.ts
0 → 100644
| 1 | +import { computed } from 'vue'; | ||
| 2 | +import { useRoute } from 'vue-router'; | ||
| 3 | + | ||
| 4 | +export const useBoardId = () => { | ||
| 5 | + const ROUTE = useRoute(); | ||
| 6 | + const boardId = computed(() => { | ||
| 7 | + return decodeURIComponent((ROUTE.params as Recordable).boardId as string); | ||
| 8 | + }); | ||
| 9 | + return { boardId }; | ||
| 10 | +}; |
| 1 | +import { unref } from 'vue'; | ||
| 2 | +import { Layout } from 'vue3-grid-layout'; | ||
| 3 | +import { DEFAULT_MAX_COL, DEFAULT_WIDGET_HEIGHT, DEFAULT_WIDGET_WIDTH } from '..'; | ||
| 4 | + | ||
| 5 | +interface GapRecord { | ||
| 6 | + maxGap: number; | ||
| 7 | + startIndex: Nullable<number>; | ||
| 8 | + endIndex: Nullable<number>; | ||
| 9 | +} | ||
| 10 | + | ||
| 11 | +export const useCalcNewWidgetPosition = ( | ||
| 12 | + layoutInfo: Layout[], | ||
| 13 | + randomLayout = { width: DEFAULT_WIDGET_WIDTH, height: DEFAULT_WIDGET_HEIGHT } | ||
| 14 | +) => { | ||
| 15 | + let maxWidth = 0; | ||
| 16 | + let maxHeight = 0; | ||
| 17 | + let maxWidthRecord = {} as unknown as Layout; | ||
| 18 | + let maxHeightRecord = {} as unknown as Layout; | ||
| 19 | + | ||
| 20 | + for (const item of unref(layoutInfo)) { | ||
| 21 | + const { x, y, h, w } = item; | ||
| 22 | + if (x + w > maxWidth) { | ||
| 23 | + maxWidth = x + w; | ||
| 24 | + maxWidthRecord = item; | ||
| 25 | + } | ||
| 26 | + if (y + h > maxHeight) { | ||
| 27 | + maxHeight = y + h; | ||
| 28 | + maxHeightRecord = item; | ||
| 29 | + } | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + maxWidth = maxWidthRecord.x + maxWidthRecord.w; | ||
| 33 | + maxHeight = maxHeightRecord.y + maxHeightRecord.h; | ||
| 34 | + | ||
| 35 | + const array = Array.from({ length: maxHeight }, (_value) => { | ||
| 36 | + return Array.from({ length: maxWidth }); | ||
| 37 | + }); | ||
| 38 | + | ||
| 39 | + for (const item of layoutInfo) { | ||
| 40 | + const { x, y, w, h } = item; | ||
| 41 | + | ||
| 42 | + for (let i = 0; i < h; i++) { | ||
| 43 | + const rowIndex = y + i > array.length - 1 ? array.length - 1 : y + i; | ||
| 44 | + const colEnd = x + w; | ||
| 45 | + const row = array[rowIndex]; | ||
| 46 | + row.fill(true, x, colEnd); | ||
| 47 | + } | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + const checkAreaIsAvaliable = (rowIndex: number, rowRecord: GapRecord[]) => { | ||
| 51 | + const { height } = randomLayout; | ||
| 52 | + | ||
| 53 | + for (const { startIndex: colStartIndex } of rowRecord) { | ||
| 54 | + let record: GapRecord = { maxGap: 0, startIndex: null, endIndex: null }; | ||
| 55 | + const heightGapRecord: GapRecord[] = []; | ||
| 56 | + for (let i = 0; i < height; i++) { | ||
| 57 | + const rowStartIndex = rowIndex + i > array.length - 1 ? array.length - 1 : rowIndex + i; | ||
| 58 | + const row = array[rowStartIndex]; | ||
| 59 | + const col = row[colStartIndex!]; | ||
| 60 | + if (col) { | ||
| 61 | + if (record.maxGap > 0) heightGapRecord.push(record); | ||
| 62 | + record = { maxGap: 0, startIndex: null, endIndex: null }; | ||
| 63 | + } | ||
| 64 | + if (!col) { | ||
| 65 | + record = { | ||
| 66 | + maxGap: record.maxGap + 1, | ||
| 67 | + startIndex: record.startIndex === null ? rowStartIndex : record.startIndex, | ||
| 68 | + endIndex: rowStartIndex, | ||
| 69 | + }; | ||
| 70 | + } | ||
| 71 | + if (i + 1 === height) if (record.maxGap > 0) heightGapRecord.push(record); | ||
| 72 | + } | ||
| 73 | + const minHeight = heightGapRecord.length | ||
| 74 | + ? Math.min(...heightGapRecord.map((item) => item.maxGap)) | ||
| 75 | + : 0; | ||
| 76 | + if (minHeight >= height) { | ||
| 77 | + let flag = true; | ||
| 78 | + for (let colIndex = colStartIndex!; colIndex < record.endIndex!; colIndex++) { | ||
| 79 | + for (let _rowIndex = rowIndex; _rowIndex < height; _rowIndex++) { | ||
| 80 | + if (array[_rowIndex][colIndex]) { | ||
| 81 | + flag = false; | ||
| 82 | + break; | ||
| 83 | + } | ||
| 84 | + } | ||
| 85 | + } | ||
| 86 | + if (flag) return { y: rowIndex, x: colStartIndex!, flag: true }; | ||
| 87 | + } | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + return { flag: false, x: 0, y: 0 }; | ||
| 91 | + }; | ||
| 92 | + | ||
| 93 | + for (let rowIndex = 0; rowIndex < array.length; rowIndex++) { | ||
| 94 | + const row = array[rowIndex]; | ||
| 95 | + let record: GapRecord = { maxGap: 0, startIndex: null, endIndex: null }; | ||
| 96 | + const widthGapRecord: GapRecord[] = []; | ||
| 97 | + | ||
| 98 | + const { width } = unref(randomLayout); | ||
| 99 | + | ||
| 100 | + for (let colIndex = 0; colIndex < DEFAULT_MAX_COL; colIndex++) { | ||
| 101 | + const col = row[colIndex]; | ||
| 102 | + if (col) { | ||
| 103 | + if (record.maxGap > 0) widthGapRecord.push(record); | ||
| 104 | + record = { maxGap: 0, startIndex: null, endIndex: null }; | ||
| 105 | + } | ||
| 106 | + if (!col) { | ||
| 107 | + record = { | ||
| 108 | + maxGap: record.maxGap + 1, | ||
| 109 | + startIndex: record.startIndex === null ? colIndex : record.startIndex, | ||
| 110 | + endIndex: colIndex, | ||
| 111 | + }; | ||
| 112 | + } | ||
| 113 | + if (colIndex + 1 === DEFAULT_MAX_COL) if (record.maxGap > 0) widthGapRecord.push(record); | ||
| 114 | + } | ||
| 115 | + | ||
| 116 | + const maxWidth = widthGapRecord.length | ||
| 117 | + ? Math.max(...widthGapRecord.map((item) => item.maxGap)) | ||
| 118 | + : 0; | ||
| 119 | + | ||
| 120 | + if (maxWidth >= width) { | ||
| 121 | + const maxRecordList = widthGapRecord.filter((item) => item.maxGap >= maxWidth); | ||
| 122 | + const { flag, x, y } = checkAreaIsAvaliable(rowIndex, maxRecordList); | ||
| 123 | + if (flag) return { x, y, w: randomLayout.width, h: randomLayout.height }; | ||
| 124 | + } | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + return { x: 0, y: array.length, w: randomLayout.width, h: randomLayout.height }; | ||
| 128 | +}; |
| 1 | +import { ComputedRef, computed, onMounted, ref, unref } from 'vue'; | ||
| 2 | +import { useRoute } from 'vue-router'; | ||
| 3 | +import { getDataComponent, updateDataBoardLayout } from '/@/api/dataBoard'; | ||
| 4 | +import { decode } from '../../board/config/config'; | ||
| 5 | +import { Layout } from 'vue3-grid-layout'; | ||
| 6 | +import { | ||
| 7 | + ApiDataBoardDataType, | ||
| 8 | + ApiDataBoardInfoType, | ||
| 9 | + ComponentDataType, | ||
| 10 | + ComponentLayoutType, | ||
| 11 | +} from '../types'; | ||
| 12 | +import { buildUUID } from '/@/utils/uuid'; | ||
| 13 | +import { PublicComponentOptions } from '../../packages/index.type'; | ||
| 14 | + | ||
| 15 | +export interface WidgetDataType extends Layout, ComponentDataType, PublicComponentOptions {} | ||
| 16 | + | ||
| 17 | +export const useDataSource = (propsRef: ComputedRef<Recordable>) => { | ||
| 18 | + const ROUTE = useRoute(); | ||
| 19 | + | ||
| 20 | + const loading = ref(false); | ||
| 21 | + | ||
| 22 | + const rawDataSource = ref<ApiDataBoardDataType>({} as ApiDataBoardDataType); | ||
| 23 | + | ||
| 24 | + const dataSource = ref<WidgetDataType[]>([]); | ||
| 25 | + | ||
| 26 | + const getSharePageData = computed(() => { | ||
| 27 | + return unref(propsRef).value!; | ||
| 28 | + }); | ||
| 29 | + | ||
| 30 | + const getIsSharePage = computed(() => { | ||
| 31 | + return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId'); | ||
| 32 | + }); | ||
| 33 | + | ||
| 34 | + const getBoardId = computed(() => { | ||
| 35 | + return decode((ROUTE.params as { boardId: string }).boardId); | ||
| 36 | + }); | ||
| 37 | + | ||
| 38 | + const getBasePageComponentData = async () => { | ||
| 39 | + try { | ||
| 40 | + return (await getDataComponent(unref(getBoardId))) as unknown as ApiDataBoardInfoType; | ||
| 41 | + } catch (error) {} | ||
| 42 | + return {} as ApiDataBoardInfoType; | ||
| 43 | + }; | ||
| 44 | + | ||
| 45 | + const getDataBoradDetail = async () => { | ||
| 46 | + try { | ||
| 47 | + return unref(getIsSharePage) | ||
| 48 | + ? (unref(getSharePageData) as ApiDataBoardInfoType) | ||
| 49 | + : await getBasePageComponentData(); | ||
| 50 | + } catch (error) { | ||
| 51 | + return {} as ApiDataBoardInfoType; | ||
| 52 | + } | ||
| 53 | + }; | ||
| 54 | + | ||
| 55 | + /** | ||
| 56 | + * @description 获取数据源 | ||
| 57 | + */ | ||
| 58 | + const getDataSource = async () => { | ||
| 59 | + try { | ||
| 60 | + loading.value = true; | ||
| 61 | + const { data } = await getDataBoradDetail(); | ||
| 62 | + rawDataSource.value = data; | ||
| 63 | + const result: WidgetDataType[] = data.componentLayout.map((item) => { | ||
| 64 | + const { id } = item; | ||
| 65 | + const dataSource = | ||
| 66 | + data.componentData.find((item) => item.id == id) || ({} as ComponentDataType); | ||
| 67 | + | ||
| 68 | + dataSource.dataSource = dataSource.dataSource.map((item) => ({ | ||
| 69 | + ...item, | ||
| 70 | + uuid: buildUUID(), | ||
| 71 | + })); | ||
| 72 | + return { | ||
| 73 | + i: buildUUID(), | ||
| 74 | + ...item, | ||
| 75 | + ...dataSource, | ||
| 76 | + } as WidgetDataType; | ||
| 77 | + }); | ||
| 78 | + dataSource.value = result; | ||
| 79 | + return result; | ||
| 80 | + } catch (error) { | ||
| 81 | + throw error; | ||
| 82 | + } finally { | ||
| 83 | + loading.value = false; | ||
| 84 | + } | ||
| 85 | + }; | ||
| 86 | + | ||
| 87 | + /** | ||
| 88 | + * @description 获取页面中的所有组件坐标 | ||
| 89 | + */ | ||
| 90 | + const getLayoutInfo = () => { | ||
| 91 | + return unref(dataSource).map((item) => { | ||
| 92 | + return { | ||
| 93 | + id: item.id, | ||
| 94 | + h: item.h, | ||
| 95 | + w: item.w, | ||
| 96 | + x: item.x, | ||
| 97 | + y: item.y, | ||
| 98 | + } as ComponentLayoutType; | ||
| 99 | + }); | ||
| 100 | + }; | ||
| 101 | + | ||
| 102 | + /** | ||
| 103 | + * @description 更新坐标 | ||
| 104 | + */ | ||
| 105 | + const setLayoutInfo = async () => { | ||
| 106 | + try { | ||
| 107 | + await updateDataBoardLayout({ | ||
| 108 | + boardId: unref(getBoardId), | ||
| 109 | + layout: getLayoutInfo(), | ||
| 110 | + }); | ||
| 111 | + } catch (error) {} | ||
| 112 | + }; | ||
| 113 | + | ||
| 114 | + onMounted(() => { | ||
| 115 | + getDataSource(); | ||
| 116 | + }); | ||
| 117 | + | ||
| 118 | + return { | ||
| 119 | + loading, | ||
| 120 | + draggable: !unref(getIsSharePage), | ||
| 121 | + resizable: !unref(getIsSharePage), | ||
| 122 | + dataSource, | ||
| 123 | + rawDataSource, | ||
| 124 | + getDataSource, | ||
| 125 | + setLayoutInfo, | ||
| 126 | + }; | ||
| 127 | +}; |
| 1 | +import { throttle } from 'lodash-es'; | ||
| 2 | +import { Ref, unref } from 'vue'; | ||
| 3 | +import { WidgetDataType } from './useDataSource'; | ||
| 4 | + | ||
| 5 | +export const useDragGridLayout = ( | ||
| 6 | + dataSourceRef: Ref<WidgetDataType[]>, | ||
| 7 | + setLayoutInfo: () => Promise<void> | ||
| 8 | +) => { | ||
| 9 | + const findUpdateItem = (i: string) => unref(dataSourceRef).find((item) => item.i === i); | ||
| 10 | + | ||
| 11 | + const calcLayout = (i: string, newHPx: number, newWPx: number) => { | ||
| 12 | + const record = findUpdateItem(i); | ||
| 13 | + | ||
| 14 | + if (!record) return; | ||
| 15 | + | ||
| 16 | + const length = record.dataSource.length || 0; | ||
| 17 | + | ||
| 18 | + const row = Math.floor(Math.pow(length, 0.5)); | ||
| 19 | + const col = Math.floor(length / row); | ||
| 20 | + let width = Number((100 / col).toFixed(2)); | ||
| 21 | + let height = Number((100 / row).toFixed(2)); | ||
| 22 | + | ||
| 23 | + const WHRatio = newWPx / newHPx; | ||
| 24 | + const HWRatio = newHPx / newWPx; | ||
| 25 | + | ||
| 26 | + if (WHRatio > 1.6) { | ||
| 27 | + width = Number((100 / length).toFixed(2)); | ||
| 28 | + height = 100; | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + if (HWRatio > 1.6) { | ||
| 32 | + height = Number((100 / length).toFixed(2)); | ||
| 33 | + width = 100; | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + const widgetWrapperHeader = 68; | ||
| 37 | + // const widgetWrapperTitle = 40; | ||
| 38 | + const widgetWrapperTitle = 0; | ||
| 39 | + let offsetHeight = widgetWrapperHeader; | ||
| 40 | + if (record.name) { | ||
| 41 | + offsetHeight = offsetHeight + widgetWrapperTitle; | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + record.widthPx = newWPx; | ||
| 45 | + record.heightPx = newHPx - offsetHeight; | ||
| 46 | + record.itemWidthRatio = width; | ||
| 47 | + record.itemHeightRatio = height; | ||
| 48 | + }; | ||
| 49 | + | ||
| 50 | + const resized = (_i: string, _newH: number, _newW: number, _newHPx: string, _newWPx: string) => { | ||
| 51 | + setLayoutInfo(); | ||
| 52 | + }; | ||
| 53 | + | ||
| 54 | + const resize = throttle( | ||
| 55 | + (i: string, _newH: number, _newW: number, newHPx: number, newWPx: number) => { | ||
| 56 | + calcLayout(i, Number(newHPx), Number(newWPx)); | ||
| 57 | + }, | ||
| 58 | + 50 | ||
| 59 | + ); | ||
| 60 | + | ||
| 61 | + const moved = (_i: string, _newX: number, _newY: number) => { | ||
| 62 | + setLayoutInfo(); | ||
| 63 | + }; | ||
| 64 | + | ||
| 65 | + const containerResized = ( | ||
| 66 | + i: string, | ||
| 67 | + _newH: number, | ||
| 68 | + _newW: number, | ||
| 69 | + newHPx: string, | ||
| 70 | + newWPx: string | ||
| 71 | + ) => { | ||
| 72 | + calcLayout(i, Number(newHPx), Number(newWPx)); | ||
| 73 | + }; | ||
| 74 | + | ||
| 75 | + return { | ||
| 76 | + resize, | ||
| 77 | + containerResized, | ||
| 78 | + moved, | ||
| 79 | + resized, | ||
| 80 | + }; | ||
| 81 | +}; |
src/views/visual/palette/hooks/useShare.ts
0 → 100644
| 1 | +import { computed } from 'vue'; | ||
| 2 | +import { useRoute } from 'vue-router'; | ||
| 3 | + | ||
| 4 | +export const useShare = () => { | ||
| 5 | + const ROUTE = useRoute(); | ||
| 6 | + const getIsSharePage = computed(() => { | ||
| 7 | + return ROUTE.matched.find((item) => item.path === '/share/:viewType/:id/:publicId'); | ||
| 8 | + }); | ||
| 9 | + | ||
| 10 | + return { getIsSharePage }; | ||
| 11 | +}; |
src/views/visual/palette/index.ts
0 → 100644
| 1 | +export { default as Palette } from './index.vue'; | ||
| 2 | + | ||
| 3 | +export const paletteInitSize = { | ||
| 4 | + w: 1920, | ||
| 5 | + h: 1080, | ||
| 6 | +}; | ||
| 7 | + | ||
| 8 | +export const DEFAULT_MAX_COL = 24; | ||
| 9 | +export const DEFAULT_WIDGET_WIDTH = 6; | ||
| 10 | +export const DEFAULT_WIDGET_HEIGHT = 6; | ||
| 11 | +export const DEFAULT_MIN_HEIGHT = 5; | ||
| 12 | +export const DEFAULT_MIN_WIDTH = 3; | ||
| 13 | +export const DEFAULT_ITEM_MARIGN = 20; | ||
| 14 | + | ||
| 15 | +import { ViewTypeEnum } from '/@/views/sys/share/config/config'; | ||
| 16 | + | ||
| 17 | +export enum MoreActionEvent { | ||
| 18 | + EDIT = 'edit', | ||
| 19 | + COPY = 'copy', | ||
| 20 | + DELETE = 'delete', | ||
| 21 | + SHARE = 'share', | ||
| 22 | +} | ||
| 23 | + | ||
| 24 | +export enum VisualBoardPermission { | ||
| 25 | + UPDATE = 'api:yt:data_board:update:update', | ||
| 26 | + DELETE = 'api:yt:data_board:delete', | ||
| 27 | + CREATE = '', | ||
| 28 | + DETAIL = 'api:yt:data_component:list', | ||
| 29 | +} | ||
| 30 | + | ||
| 31 | +export enum VisualComponentPermission { | ||
| 32 | + UPDATE = 'api:yt:data_component:update:update', | ||
| 33 | + DELETE = 'api:yt:data_component:delete', | ||
| 34 | + COPY = 'api:yt:dataBoardDetail:copy', | ||
| 35 | + CREATE = 'api:yt:data_component:add:post', | ||
| 36 | +} | ||
| 37 | + | ||
| 38 | +export const DATA_BOARD_SHARE_URL = (id: string, publicId: string) => | ||
| 39 | + `/share/${ViewTypeEnum.DATA_BOARD}/${id}/${publicId}`; | ||
| 40 | + | ||
| 41 | +export const isBataBoardSharePage = (url: string) => { | ||
| 42 | + const reg = /^\/data\/board\/share/g; | ||
| 43 | + return reg.test(url); | ||
| 44 | +}; | ||
| 45 | + | ||
| 46 | +export const encode = (string: string) => { | ||
| 47 | + return encodeURIComponent(string); | ||
| 48 | +}; | ||
| 49 | + | ||
| 50 | +export const decode = (string: string) => { | ||
| 51 | + return decodeURIComponent(string); | ||
| 52 | +}; |
src/views/visual/palette/index.vue
0 → 100644
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { onMounted, unref } from 'vue'; | ||
| 3 | + import { ref } from 'vue'; | ||
| 4 | + import { Empty, Spin, Button } from 'ant-design-vue'; | ||
| 5 | + import { getBoundingClientRect } from '/@/utils/domUtils'; | ||
| 6 | + import { GridItem, GridLayout } from 'vue3-grid-layout'; | ||
| 7 | + import { | ||
| 8 | + DEFAULT_MAX_COL, | ||
| 9 | + DEFAULT_MIN_HEIGHT, | ||
| 10 | + DEFAULT_MIN_WIDTH, | ||
| 11 | + DEFAULT_ITEM_MARIGN, | ||
| 12 | + VisualComponentPermission, | ||
| 13 | + } from './index'; | ||
| 14 | + import { useDragGridLayout } from './hooks/useDragGridLayout'; | ||
| 15 | + import { WidgetHeader, WidgetWrapper } from './components/WidgetWrapper'; | ||
| 16 | + import { computed } from 'vue'; | ||
| 17 | + import { WidgetDataType, useDataSource } from './hooks/useDataSource'; | ||
| 18 | + import { WidgetDistribute } from './components/WidgetDistribute'; | ||
| 19 | + import { DataSourceBindPanel } from '../dataSourceBindPanel'; | ||
| 20 | + import { PageHeader } from './components/PagerHeader'; | ||
| 21 | + import { useShare } from './hooks/useShare'; | ||
| 22 | + import { useRole } from '/@/hooks/business/useRole'; | ||
| 23 | + import { Authority } from '/@/components/Authority'; | ||
| 24 | + import { useModal } from '/@/components/Modal'; | ||
| 25 | + import { ModalParamsType } from '/#/utils'; | ||
| 26 | + import { DataActionModeEnum } from '/@/enums/toolEnum'; | ||
| 27 | + import { HistoryTrendModal } from './components/HistoryTrendModal'; | ||
| 28 | + import { useSocket } from '../packages/hook/useSocket'; | ||
| 29 | + import { watch } from 'vue'; | ||
| 30 | + | ||
| 31 | + const props = defineProps<{ | ||
| 32 | + value?: Recordable; | ||
| 33 | + }>(); | ||
| 34 | + | ||
| 35 | + const getProps = computed(() => props); | ||
| 36 | + | ||
| 37 | + const containerRefEl = ref<Nullable<HTMLDivElement>>(null); | ||
| 38 | + | ||
| 39 | + const containerRectRef = ref<DOMRect>({} as unknown as DOMRect); | ||
| 40 | + | ||
| 41 | + const { loading, draggable, resizable, dataSource, rawDataSource, setLayoutInfo, getDataSource } = | ||
| 42 | + useDataSource(getProps); | ||
| 43 | + | ||
| 44 | + const { resize, resized, moved, containerResized } = useDragGridLayout(dataSource, setLayoutInfo); | ||
| 45 | + | ||
| 46 | + const [register, { openModal }] = useModal(); | ||
| 47 | + | ||
| 48 | + const [registerTrendModal, { openModal: openTrendModal }] = useModal(); | ||
| 49 | + | ||
| 50 | + /** | ||
| 51 | + * @description 获取画板宽高 | ||
| 52 | + */ | ||
| 53 | + onMounted(() => { | ||
| 54 | + const rect = getBoundingClientRect(unref(containerRefEl)!); | ||
| 55 | + if (rect) { | ||
| 56 | + containerRectRef.value = rect as DOMRect; | ||
| 57 | + } | ||
| 58 | + }); | ||
| 59 | + const { getIsSharePage } = useShare(); | ||
| 60 | + const { isCustomerUser } = useRole(); | ||
| 61 | + const handleOpenCreatePanel = () => { | ||
| 62 | + openModal(true, { mode: DataActionModeEnum.CREATE } as ModalParamsType); | ||
| 63 | + }; | ||
| 64 | + | ||
| 65 | + const handleUpdateWidget = (data: WidgetDataType) => { | ||
| 66 | + openModal(true, { mode: DataActionModeEnum.UPDATE, record: data } as ModalParamsType); | ||
| 67 | + }; | ||
| 68 | + | ||
| 69 | + const handleOpenTrend = (data: WidgetDataType) => { | ||
| 70 | + openTrendModal(true, { mode: DataActionModeEnum.READ, record: data } as ModalParamsType); | ||
| 71 | + }; | ||
| 72 | + | ||
| 73 | + useSocket(dataSource); | ||
| 74 | + | ||
| 75 | + watch( | ||
| 76 | + getIsSharePage, | ||
| 77 | + (value) => { | ||
| 78 | + if (value) { | ||
| 79 | + const root = document.querySelector('#app'); | ||
| 80 | + (root as HTMLDivElement).style.backgroundColor = '#F5F5F5'; | ||
| 81 | + } | ||
| 82 | + }, | ||
| 83 | + { immediate: true } | ||
| 84 | + ); | ||
| 85 | +</script> | ||
| 86 | + | ||
| 87 | +<template> | ||
| 88 | + <section | ||
| 89 | + ref="containerRefEl" | ||
| 90 | + class="palette w-full h-full flex-col bg-neutral-100 flex dark:bg-dark-700 dark:text-light-50" | ||
| 91 | + > | ||
| 92 | + <PageHeader :widget-number="dataSource.length"> | ||
| 93 | + <Authority :value="VisualComponentPermission.CREATE"> | ||
| 94 | + <Button | ||
| 95 | + v-if="!getIsSharePage && !isCustomerUser" | ||
| 96 | + type="primary" | ||
| 97 | + @click="handleOpenCreatePanel" | ||
| 98 | + > | ||
| 99 | + 创建组件 | ||
| 100 | + </Button> | ||
| 101 | + </Authority> | ||
| 102 | + </PageHeader> | ||
| 103 | + | ||
| 104 | + <Spin :spinning="loading"> | ||
| 105 | + <GridLayout | ||
| 106 | + v-model:layout="dataSource" | ||
| 107 | + :col-num="DEFAULT_MAX_COL" | ||
| 108 | + :row-height="30" | ||
| 109 | + :margin="[DEFAULT_ITEM_MARIGN, DEFAULT_ITEM_MARIGN]" | ||
| 110 | + :is-draggable="draggable" | ||
| 111 | + :is-resizable="resizable" | ||
| 112 | + :vertical-compact="true" | ||
| 113 | + :use-css-transforms="true" | ||
| 114 | + style="width: 100%" | ||
| 115 | + > | ||
| 116 | + <GridItem | ||
| 117 | + v-for="item in dataSource" | ||
| 118 | + :key="item.i" | ||
| 119 | + :static="item.static" | ||
| 120 | + :x="item.x" | ||
| 121 | + :y="item.y" | ||
| 122 | + :w="item.w" | ||
| 123 | + :h="item.h" | ||
| 124 | + :i="item.i" | ||
| 125 | + :min-h="DEFAULT_MIN_HEIGHT" | ||
| 126 | + :min-w="DEFAULT_MIN_WIDTH" | ||
| 127 | + :style="{ display: 'flex', flexWrap: 'wrap' }" | ||
| 128 | + class="grid-item-layout" | ||
| 129 | + @resized="resized" | ||
| 130 | + @resize="resize" | ||
| 131 | + @moved="moved" | ||
| 132 | + @container-resized="containerResized" | ||
| 133 | + drag-ignore-from=".no-drag" | ||
| 134 | + > | ||
| 135 | + <WidgetWrapper> | ||
| 136 | + <template #header> | ||
| 137 | + <WidgetHeader | ||
| 138 | + :raw-data-source="rawDataSource" | ||
| 139 | + :source-info="item" | ||
| 140 | + @update="handleUpdateWidget" | ||
| 141 | + @open-trend="handleOpenTrend" | ||
| 142 | + @ok="getDataSource" | ||
| 143 | + /> | ||
| 144 | + </template> | ||
| 145 | + <WidgetDistribute :source-info="item" /> | ||
| 146 | + </WidgetWrapper> | ||
| 147 | + </GridItem> | ||
| 148 | + </GridLayout> | ||
| 149 | + <Empty | ||
| 150 | + v-if="!dataSource.length" | ||
| 151 | + class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" | ||
| 152 | + /> | ||
| 153 | + </Spin> | ||
| 154 | + | ||
| 155 | + <DataSourceBindPanel @register="register" :layout="dataSource" @ok="getDataSource" /> | ||
| 156 | + | ||
| 157 | + <HistoryTrendModal @register="registerTrendModal" /> | ||
| 158 | + </section> | ||
| 159 | +</template> | ||
| 160 | + | ||
| 161 | +<style lang="less" scoped></style> |
src/views/visual/palette/types/index.ts
0 → 100644
| 1 | +import { PublicComponentOptions } from '../../packages/index.type'; | ||
| 2 | + | ||
| 3 | +export interface ComponentDataType<T = ExtraDataSource> { | ||
| 4 | + id: string; | ||
| 5 | + creator: string; | ||
| 6 | + createTime: string; | ||
| 7 | + enabled: boolean; | ||
| 8 | + tenantId: string; | ||
| 9 | + dataBoardId: string; | ||
| 10 | + frontId: string; | ||
| 11 | + dataSource: T[]; | ||
| 12 | + name?: string; | ||
| 13 | + remark?: string; | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +export interface DataSource { | ||
| 17 | + deviceProfileId: string; | ||
| 18 | + organizationId: string; | ||
| 19 | + deviceId: string; | ||
| 20 | + deviceType: string; | ||
| 21 | + attribute: string; | ||
| 22 | + deviceName: string; | ||
| 23 | + gatewayDevice: boolean; | ||
| 24 | + slaveDeviceId: string; | ||
| 25 | + deviceRename: string; | ||
| 26 | + attributeRename: string; | ||
| 27 | + componentInfo: ComponentInfo; | ||
| 28 | + customCommand: CustomCommand; | ||
| 29 | +} | ||
| 30 | + | ||
| 31 | +export interface ExtraDataSource extends DataSource, PublicComponentOptions { | ||
| 32 | + uuid: string; | ||
| 33 | +} | ||
| 34 | + | ||
| 35 | +export interface ComponentInfoGradientInfoType { | ||
| 36 | + key: string; | ||
| 37 | + value: number; | ||
| 38 | + color: string; | ||
| 39 | +} | ||
| 40 | + | ||
| 41 | +export interface FlowmeterConfigType { | ||
| 42 | + backgroundColor: string; | ||
| 43 | + waveFirst: string; | ||
| 44 | + waveSecond: string; | ||
| 45 | + waveThird: string; | ||
| 46 | +} | ||
| 47 | + | ||
| 48 | +export interface ComponentInfo { | ||
| 49 | + fontColor: string; | ||
| 50 | + unit: string; | ||
| 51 | + icon: string; | ||
| 52 | + iconColor: string; | ||
| 53 | + showDeviceName: boolean; | ||
| 54 | + gradientInfo: ComponentInfoGradientInfoType[]; | ||
| 55 | + flowmeterConfig: FlowmeterConfigType; | ||
| 56 | +} | ||
| 57 | + | ||
| 58 | +export interface CustomCommand { | ||
| 59 | + transportType: string; | ||
| 60 | + commandType: string; | ||
| 61 | + command: string; | ||
| 62 | + service: string; | ||
| 63 | +} | ||
| 64 | + | ||
| 65 | +export interface ComponentLayoutType { | ||
| 66 | + x: number; | ||
| 67 | + y: number; | ||
| 68 | + w: number; | ||
| 69 | + h: number; | ||
| 70 | + id: string; | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +export interface ApiDataBoardDataType { | ||
| 74 | + componentData: ComponentDataType[]; | ||
| 75 | + componentLayout: ComponentLayoutType[]; | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +export interface ApiDataBoardInfoType { | ||
| 79 | + data: ApiDataBoardDataType; | ||
| 80 | +} |
| 1 | +export { default as StageFrame } from './index.vue'; |
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { Card } from 'ant-design-vue'; | ||
| 3 | + // :style="{ borderColor: props.controlId === props.checkedId ? '#3079FF' : '#fff' }" | ||
| 4 | +</script> | ||
| 5 | + | ||
| 6 | +<template> | ||
| 7 | + <Card | ||
| 8 | + hoverable | ||
| 9 | + bordered | ||
| 10 | + class="strage-frame border-2 widget-select !bg-light-50 dark:bg-dark-400 cursor-pointer" | ||
| 11 | + > | ||
| 12 | + <div class="stage-frame-container w-full h-full justify-center items-center w-60 h-50"> | ||
| 13 | + <slot></slot> | ||
| 14 | + </div> | ||
| 15 | + <Card.Meta> | ||
| 16 | + <template #description> | ||
| 17 | + <slot name="description"></slot> | ||
| 18 | + </template> | ||
| 19 | + </Card.Meta> | ||
| 20 | + </Card> | ||
| 21 | +</template> | ||
| 22 | + | ||
| 23 | +<style lang="less" scoped> | ||
| 24 | + .strage-frame { | ||
| 25 | + @apply flex justify-center items-center flex-col box-border border-light-50 dark:border-gray-700; | ||
| 26 | + | ||
| 27 | + box-shadow: 0 1px 10px 0 #0000001a !important; | ||
| 28 | + width: 244px; | ||
| 29 | + height: 244px; | ||
| 30 | + | ||
| 31 | + :deep(.ant-card-body) { | ||
| 32 | + @apply p-0 dark:bg-dark-900; | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + :deep(.ant-card-meta) { | ||
| 36 | + @apply m-0 leading-10; | ||
| 37 | + } | ||
| 38 | + } | ||
| 39 | +</style> |
src/views/visual/widgetLibrary/index.ts
0 → 100644
| 1 | +export { default as WidgetLibrary } from './index.vue'; |
src/views/visual/widgetLibrary/index.vue
0 → 100644
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { StageFrame } from './components/StageFrame'; | ||
| 3 | + import { packageList } from '../packages/package'; | ||
| 4 | + import { computed, ref } from 'vue'; | ||
| 5 | + import { Tabs } from 'ant-design-vue'; | ||
| 6 | + import { | ||
| 7 | + ConfigType, | ||
| 8 | + PackagesCategoryEnum, | ||
| 9 | + PackagesCategoryNameEnum, | ||
| 10 | + } from '../packages/index.type'; | ||
| 11 | + import { fetchViewComponent, createComponent } from '../packages'; | ||
| 12 | + import { componentMap, registerComponent } from '../packages/componentMap'; | ||
| 13 | + import { watchEffect } from 'vue'; | ||
| 14 | + import { unref } from 'vue'; | ||
| 15 | + import { watch } from 'vue'; | ||
| 16 | + | ||
| 17 | + interface PropsCheckedType { | ||
| 18 | + componentKey: string; | ||
| 19 | + categoryKey: string; | ||
| 20 | + } | ||
| 21 | + | ||
| 22 | + const props = defineProps<{ | ||
| 23 | + checked?: PropsCheckedType; | ||
| 24 | + }>(); | ||
| 25 | + | ||
| 26 | + const emit = defineEmits<{ | ||
| 27 | + (event: 'update:checked', value: PropsCheckedType): void; | ||
| 28 | + }>(); | ||
| 29 | + | ||
| 30 | + const activeKey = ref(PackagesCategoryEnum.TEXT); | ||
| 31 | + | ||
| 32 | + const getCategory = computed(() => { | ||
| 33 | + return Object.keys(packageList).map((key) => { | ||
| 34 | + const category = packageList[key] as ConfigType[]; | ||
| 35 | + return { | ||
| 36 | + title: PackagesCategoryNameEnum[PackagesCategoryEnum[key]], | ||
| 37 | + key, | ||
| 38 | + items: category, | ||
| 39 | + }; | ||
| 40 | + }); | ||
| 41 | + }); | ||
| 42 | + | ||
| 43 | + const getBindConfig = (key: string) => { | ||
| 44 | + const config = createComponent({ key } as ConfigType); | ||
| 45 | + return { | ||
| 46 | + config, | ||
| 47 | + }; | ||
| 48 | + }; | ||
| 49 | + | ||
| 50 | + watchEffect(() => { | ||
| 51 | + unref(getCategory).forEach((category) => { | ||
| 52 | + category.items.forEach((item) => { | ||
| 53 | + const { key } = item; | ||
| 54 | + const component = fetchViewComponent({ key } as ConfigType); | ||
| 55 | + registerComponent(key, component); | ||
| 56 | + }); | ||
| 57 | + }); | ||
| 58 | + }); | ||
| 59 | + | ||
| 60 | + const handleSelected = (item: ConfigType) => { | ||
| 61 | + const { key, package: packageKey } = item; | ||
| 62 | + emit('update:checked', { categoryKey: packageKey!, componentKey: key }); | ||
| 63 | + }; | ||
| 64 | + | ||
| 65 | + watch( | ||
| 66 | + () => props.checked?.categoryKey, | ||
| 67 | + (value) => { | ||
| 68 | + if (value) activeKey.value = value as PackagesCategoryEnum; | ||
| 69 | + } | ||
| 70 | + ); | ||
| 71 | +</script> | ||
| 72 | + | ||
| 73 | +<template> | ||
| 74 | + <section> | ||
| 75 | + <Tabs v-model:activeKey="activeKey"> | ||
| 76 | + <Tabs.TabPane | ||
| 77 | + v-for="item in getCategory" | ||
| 78 | + :key="item.key" | ||
| 79 | + :tab="item.title" | ||
| 80 | + :forceRender="false" | ||
| 81 | + > | ||
| 82 | + <main class="flex min-h-64 px-2 gap-8 flex-wrap justify-start"> | ||
| 83 | + <StageFrame | ||
| 84 | + class="mt-4" | ||
| 85 | + v-for="temp in item.items" | ||
| 86 | + :key="temp.key" | ||
| 87 | + @click="handleSelected(temp)" | ||
| 88 | + :class="temp.key === props.checked?.componentKey ? '!border-2 !border-blue-500' : ''" | ||
| 89 | + > | ||
| 90 | + <component :is="componentMap.get(temp.key)" v-bind="getBindConfig(temp.key)" /> | ||
| 91 | + <template #description> | ||
| 92 | + <div class="h-10 leading-10 text-center border-t border-light-600 border-solid"> | ||
| 93 | + {{ temp.title }} | ||
| 94 | + </div> | ||
| 95 | + </template> | ||
| 96 | + </StageFrame> | ||
| 97 | + </main> | ||
| 98 | + </Tabs.TabPane> | ||
| 99 | + </Tabs> | ||
| 100 | + </section> | ||
| 101 | +</template> |