Showing
34 changed files
with
2043 additions
and
1 deletions
@@ -84,6 +84,7 @@ | @@ -84,6 +84,7 @@ | ||
84 | "@types/inquirer": "^7.3.3", | 84 | "@types/inquirer": "^7.3.3", |
85 | "@types/intro.js": "^3.0.2", | 85 | "@types/intro.js": "^3.0.2", |
86 | "@types/jest": "^27.0.1", | 86 | "@types/jest": "^27.0.1", |
87 | + "@types/jsoneditor": "^9.9.0", | ||
87 | "@types/lodash-es": "^4.17.4", | 88 | "@types/lodash-es": "^4.17.4", |
88 | "@types/mockjs": "^1.0.4", | 89 | "@types/mockjs": "^1.0.4", |
89 | "@types/node": "^16.6.1", | 90 | "@types/node": "^16.6.1", |
src/api/task/index.ts
0 → 100644
1 | +import { CreateTaskRecordType, GetTaskListParamsType, TaskRecordType } from './model'; | ||
2 | +import { PaginationResult } from '/#/axios'; | ||
3 | +import { defHttp } from '/@/utils/http/axios'; | ||
4 | + | ||
5 | +enum Api { | ||
6 | + TASK_LIST = '/task_center', | ||
7 | + ADD_TASK = '/task_center/add', | ||
8 | + UPDATE_STATE = '/task_center', | ||
9 | + DELETE_TASK = '/task_center', | ||
10 | + UPDATE_TASK = '/task_center/update', | ||
11 | + CANCEL_TASK = '/task_center', | ||
12 | +} | ||
13 | + | ||
14 | +export const getTaskCenterList = (params: GetTaskListParamsType) => { | ||
15 | + return defHttp.get<PaginationResult<TaskRecordType>>({ | ||
16 | + url: Api.TASK_LIST, | ||
17 | + params, | ||
18 | + }); | ||
19 | +}; | ||
20 | + | ||
21 | +export const createTask = (data: CreateTaskRecordType) => { | ||
22 | + return defHttp.post({ | ||
23 | + url: Api.ADD_TASK, | ||
24 | + data, | ||
25 | + }); | ||
26 | +}; | ||
27 | + | ||
28 | +export const updateState = (id: string, state: number) => { | ||
29 | + return defHttp.put({ | ||
30 | + url: `${Api.UPDATE_STATE}/${id}/update/${state}`, | ||
31 | + }); | ||
32 | +}; | ||
33 | + | ||
34 | +export const deleteTask = (ids: string[]) => { | ||
35 | + return defHttp.delete({ | ||
36 | + url: Api.DELETE_TASK, | ||
37 | + data: { ids }, | ||
38 | + }); | ||
39 | +}; | ||
40 | + | ||
41 | +export const updateTask = (data: CreateTaskRecordType & Record<'id', string>) => { | ||
42 | + return defHttp.put({ | ||
43 | + url: Api.UPDATE_TASK, | ||
44 | + data, | ||
45 | + }); | ||
46 | +}; | ||
47 | + | ||
48 | +export const cancelTask = (data: Record<'id' | 'tbDeviceId', string>) => { | ||
49 | + return defHttp.put({ | ||
50 | + url: `${Api.CANCEL_TASK}/${data.id}/cancel/${data.tbDeviceId}`, | ||
51 | + }); | ||
52 | +}; |
src/api/task/model/index.ts
0 → 100644
1 | +import { TaskTargetEnum } from '/@/views/task/center/config'; | ||
2 | +import { | ||
3 | + ExecuteTimeTypeEnum, | ||
4 | + PeriodTypeEnum, | ||
5 | + PushWayEnum, | ||
6 | +} from '/@/views/task/center/components/CreateModal/config'; | ||
7 | +import { TaskTypeEnum } from '/@/views/task/center/config'; | ||
8 | + | ||
9 | +export interface GetTaskListParamsType { | ||
10 | + page: number; | ||
11 | + pageSize: number; | ||
12 | + state?: string; | ||
13 | +} | ||
14 | + | ||
15 | +export interface CreateTaskRecordType { | ||
16 | + name: string; | ||
17 | + targetType: TaskTargetEnum; | ||
18 | + executeTarget: { | ||
19 | + organizationId?: string; | ||
20 | + deviceProfileId?: string; | ||
21 | + deviceType?: string; | ||
22 | + cancelExecuteDevices?: string[]; | ||
23 | + data?: string[]; | ||
24 | + }; | ||
25 | + executeContent: { | ||
26 | + pushContent: { | ||
27 | + rpcCommand: string | Recordable; | ||
28 | + }; | ||
29 | + pushWay: PushWayEnum; | ||
30 | + type: TaskTypeEnum; | ||
31 | + }; | ||
32 | + executeTime: { | ||
33 | + cron: string; | ||
34 | + type: ExecuteTimeTypeEnum; | ||
35 | + periodType: PeriodTypeEnum; | ||
36 | + period: string; | ||
37 | + time: string; | ||
38 | + pollUnit: string; | ||
39 | + }; | ||
40 | +} | ||
41 | + | ||
42 | +export interface TaskRecordType extends CreateTaskRecordType { | ||
43 | + id: string; | ||
44 | + createTime: string; | ||
45 | + creator: string; | ||
46 | + enabled: boolean; | ||
47 | + state: number; | ||
48 | +} |
@@ -2,6 +2,7 @@ import { withInstall } from '/@/utils'; | @@ -2,6 +2,7 @@ import { withInstall } from '/@/utils'; | ||
2 | // @ts-ignore | 2 | // @ts-ignore |
3 | import codeEditor from './src/CodeEditor.vue'; | 3 | import codeEditor from './src/CodeEditor.vue'; |
4 | import jsonPreview from './src/json-preview/JsonPreview.vue'; | 4 | import jsonPreview from './src/json-preview/JsonPreview.vue'; |
5 | +export { JSONEditor } from './src/JSONEditor'; | ||
5 | 6 | ||
6 | export const CodeEditor = withInstall(codeEditor); | 7 | export const CodeEditor = withInstall(codeEditor); |
7 | export const JsonPreview = withInstall(jsonPreview); | 8 | export const JsonPreview = withInstall(jsonPreview); |
1 | +import { Rule } from '/@/components/Form'; | ||
2 | + | ||
3 | +export { default as JSONEditor } from './index.vue'; | ||
4 | + | ||
5 | +export const parseStringToJSON = <T = Recordable>(value: string) => { | ||
6 | + try { | ||
7 | + const json = JSON.parse(value) as T; | ||
8 | + return { json, valid: true }; | ||
9 | + } catch (error) { | ||
10 | + return { json: null, valid: false }; | ||
11 | + } | ||
12 | +}; | ||
13 | + | ||
14 | +export const JSONEditorValidator = (message = 'json格式校验失败'): Rule[] => { | ||
15 | + return [ | ||
16 | + { | ||
17 | + validateTrigger: 'blur', | ||
18 | + validator(_rule: Rule, value: any, _callback: Fn) { | ||
19 | + const { valid } = parseStringToJSON(value); | ||
20 | + if (valid) { | ||
21 | + return Promise.resolve(); | ||
22 | + } | ||
23 | + return Promise.reject(message); | ||
24 | + }, | ||
25 | + }, | ||
26 | + ]; | ||
27 | +}; |
1 | +<script lang="ts" setup> | ||
2 | + import { ref } from 'vue'; | ||
3 | + import JSONEditor, { JSONEditorOptions } from 'jsoneditor'; | ||
4 | + import 'jsoneditor/dist/jsoneditor.min.css'; | ||
5 | + import { unref } from 'vue'; | ||
6 | + import { onMounted } from 'vue'; | ||
7 | + import { computed } from '@vue/reactivity'; | ||
8 | + import { onUnmounted } from 'vue'; | ||
9 | + import { watch } from 'vue'; | ||
10 | + | ||
11 | + enum EventEnum { | ||
12 | + UPDATE_VALUE = 'update:value', | ||
13 | + CHANGE = 'change', | ||
14 | + BLUR = 'blur', | ||
15 | + FOCUS = 'focus', | ||
16 | + } | ||
17 | + | ||
18 | + const props = withDefaults( | ||
19 | + defineProps<{ | ||
20 | + value?: string; | ||
21 | + options?: JSONEditorOptions; | ||
22 | + }>(), | ||
23 | + { | ||
24 | + options: () => | ||
25 | + ({ | ||
26 | + mode: 'code', | ||
27 | + mainMenuBar: false, | ||
28 | + statusBar: false, | ||
29 | + } as JSONEditorOptions), | ||
30 | + } | ||
31 | + ); | ||
32 | + | ||
33 | + const emit = defineEmits<{ | ||
34 | + (e: EventEnum.UPDATE_VALUE, value: any, instance?: JSONEditor): void; | ||
35 | + (e: EventEnum.CHANGE, value: any, instance?: JSONEditor): void; | ||
36 | + (e: EventEnum.BLUR, event: Event, instance?: JSONEditor): void; | ||
37 | + (e: EventEnum.FOCUS, event: Event, instance?: JSONEditor): void; | ||
38 | + }>(); | ||
39 | + | ||
40 | + const jsonEditorElRef = ref<Nullable<any>>(); | ||
41 | + | ||
42 | + const editoreRef = ref<JSONEditor>(); | ||
43 | + | ||
44 | + const handleChange = (value: any) => { | ||
45 | + emit(EventEnum.UPDATE_VALUE, value, unref(editoreRef)); | ||
46 | + emit(EventEnum.CHANGE, value, unref(editoreRef)); | ||
47 | + }; | ||
48 | + | ||
49 | + const handleEmit = (event: Event, key: EventEnum) => { | ||
50 | + emit(key as EventEnum[keyof EventEnum], event, unref(editoreRef)); | ||
51 | + }; | ||
52 | + | ||
53 | + const getOptions = computed(() => { | ||
54 | + const { options } = props; | ||
55 | + return { | ||
56 | + ...options, | ||
57 | + onChangeText: handleChange, | ||
58 | + onBlur: (event: Event) => handleEmit(event, EventEnum.BLUR), | ||
59 | + onFocus: (event: Event) => handleEmit(event, EventEnum.FOCUS), | ||
60 | + } as JSONEditorOptions; | ||
61 | + }); | ||
62 | + | ||
63 | + const initialize = () => { | ||
64 | + editoreRef.value = new JSONEditor(unref(jsonEditorElRef), unref(getOptions)); | ||
65 | + }; | ||
66 | + | ||
67 | + watch( | ||
68 | + () => props.value, | ||
69 | + (target) => { | ||
70 | + unref(editoreRef)?.setText(target || ''); | ||
71 | + }, | ||
72 | + { | ||
73 | + immediate: true, | ||
74 | + } | ||
75 | + ); | ||
76 | + | ||
77 | + const get = (): string => { | ||
78 | + return unref(editoreRef)?.getText() || ''; | ||
79 | + }; | ||
80 | + | ||
81 | + const set = (data: any) => { | ||
82 | + return unref(editoreRef)?.set(data); | ||
83 | + }; | ||
84 | + | ||
85 | + onMounted(() => { | ||
86 | + initialize(); | ||
87 | + }); | ||
88 | + | ||
89 | + onUnmounted(() => { | ||
90 | + unref(editoreRef)?.destroy(); | ||
91 | + }); | ||
92 | + | ||
93 | + defineExpose({ | ||
94 | + get, | ||
95 | + set, | ||
96 | + }); | ||
97 | +</script> | ||
98 | + | ||
99 | +<template> | ||
100 | + <div class="p-2 bg-gray-200"> | ||
101 | + <div ref="jsonEditorElRef" class="jsoneditor"></div> | ||
102 | + </div> | ||
103 | +</template> | ||
104 | + | ||
105 | +<style lang="less" scoped> | ||
106 | + .jsoneditor { | ||
107 | + border: none !important; | ||
108 | + | ||
109 | + :deep(.jsoneditor) { | ||
110 | + border: none !important; | ||
111 | + } | ||
112 | + } | ||
113 | +</style> |
@@ -122,4 +122,8 @@ export type ComponentType = | @@ -122,4 +122,8 @@ export type ComponentType = | ||
122 | | 'ApiSelectScrollLoad' | 122 | | 'ApiSelectScrollLoad' |
123 | | 'TransferModal' | 123 | | 'TransferModal' |
124 | | 'TransferTableModal' | 124 | | 'TransferTableModal' |
125 | - | 'ObjectModelValidateForm'; | 125 | + | 'ObjectModelValidateForm' |
126 | + | 'DevicePicker' | ||
127 | + | 'CustomInput' | ||
128 | + | 'RegisterAddressInput' | ||
129 | + | 'ControlGroup'; |
src/enums/dictEnum.ts
0 → 100644
src/enums/toolEnum.ts
0 → 100644
1 | +export enum DataActionModeEnum { | ||
2 | + CREATE = 'CREATE', | ||
3 | + READ = 'READ', | ||
4 | + UPDATE = 'UPDATE', | ||
5 | + DELETE = 'DELETE', | ||
6 | +} | ||
7 | + | ||
8 | +export enum TimeUnitEnum { | ||
9 | + SECOND = 'SECOND', | ||
10 | + MINUTE = 'MINUTE', | ||
11 | + HOUR = 'HOUR', | ||
12 | +} | ||
13 | + | ||
14 | +export enum TimeUnitNameEnum { | ||
15 | + SECOND = '秒', | ||
16 | + MINUTE = '分', | ||
17 | + HOUR = '时', | ||
18 | +} |
src/utils/pickerSearch.ts
0 → 100644
1 | +export const createPickerSearch = (searchValue = false) => { | ||
2 | + return { | ||
3 | + showSearch: true, | ||
4 | + filterOption: (inputValue: string, option: Record<'label' | 'value', string>) => { | ||
5 | + let { label, value } = option; | ||
6 | + label = label.toLowerCase(); | ||
7 | + value = value.toLowerCase(); | ||
8 | + inputValue = inputValue.toLowerCase(); | ||
9 | + return label.includes(inputValue) || (searchValue && value.includes(inputValue)); | ||
10 | + }, | ||
11 | + }; | ||
12 | +}; |
@@ -51,6 +51,9 @@ | @@ -51,6 +51,9 @@ | ||
51 | <TabPane key="eventManage" tab="事件管理"> | 51 | <TabPane key="eventManage" tab="事件管理"> |
52 | <EventManage :tbDeviceId="deviceDetail.tbDeviceId" /> | 52 | <EventManage :tbDeviceId="deviceDetail.tbDeviceId" /> |
53 | </TabPane> | 53 | </TabPane> |
54 | + <TabPane key="task" tab="任务"> | ||
55 | + <Task :tbDeviceId="deviceDetail.tbDeviceId" /> | ||
56 | + </TabPane> | ||
54 | </Tabs> | 57 | </Tabs> |
55 | </BasicDrawer> | 58 | </BasicDrawer> |
56 | </template> | 59 | </template> |
@@ -71,6 +74,7 @@ | @@ -71,6 +74,7 @@ | ||
71 | import ModelOfMatter from '../tabs/ModelOfMatter.vue'; | 74 | import ModelOfMatter from '../tabs/ModelOfMatter.vue'; |
72 | import EventManage from '../tabs/EventManage/index.vue'; | 75 | import EventManage from '../tabs/EventManage/index.vue'; |
73 | import { DeviceRecord } from '/@/api/device/model/deviceModel'; | 76 | import { DeviceRecord } from '/@/api/device/model/deviceModel'; |
77 | + import Task from '../tabs/Task.vue'; | ||
74 | 78 | ||
75 | export default defineComponent({ | 79 | export default defineComponent({ |
76 | name: 'DeviceModal', | 80 | name: 'DeviceModal', |
@@ -88,6 +92,7 @@ | @@ -88,6 +92,7 @@ | ||
88 | ModelOfMatter, | 92 | ModelOfMatter, |
89 | CommandRecord, | 93 | CommandRecord, |
90 | EventManage, | 94 | EventManage, |
95 | + Task, | ||
91 | }, | 96 | }, |
92 | emits: ['reload', 'register', 'openTbDeviceDetail', 'openGatewayDeviceDetail'], | 97 | emits: ['reload', 'register', 'openTbDeviceDetail', 'openGatewayDeviceDetail'], |
93 | setup(_props, { emit }) { | 98 | setup(_props, { emit }) { |
src/views/device/list/cpns/tabs/Task.vue
0 → 100644
1 | +<script lang="ts" setup> | ||
2 | + import { ReloadOutlined } from '@ant-design/icons-vue'; | ||
3 | + import { Button, List, Tooltip } from 'ant-design-vue'; | ||
4 | + import { TaskCard } from '/@/views/task/center/components/TaskCard'; | ||
5 | + import { reactive, ref, unref } from 'vue'; | ||
6 | + import { PageWrapper } from '/@/components/Page'; | ||
7 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
8 | + import { formSchemas } from '/@/views/task/center/config'; | ||
9 | + import { getTaskCenterList } from '/@/api/task'; | ||
10 | + import { onMounted } from 'vue'; | ||
11 | + import { getBoundingClientRect } from '/@/utils/domUtils'; | ||
12 | + import { TaskRecordType } from '/@/api/task/model'; | ||
13 | + | ||
14 | + defineProps<{ | ||
15 | + tbDeviceId: string; | ||
16 | + }>(); | ||
17 | + | ||
18 | + const listElRef = ref<Nullable<ComponentElRef>>(null); | ||
19 | + | ||
20 | + const [registerForm, { getFieldsValue }] = useForm({ | ||
21 | + schemas: formSchemas, | ||
22 | + baseColProps: { span: 8 }, | ||
23 | + compact: true, | ||
24 | + showAdvancedButton: true, | ||
25 | + labelWidth: 100, | ||
26 | + submitFunc: async () => { | ||
27 | + pagination.params = getFieldsValue(); | ||
28 | + getDataSource(); | ||
29 | + }, | ||
30 | + }); | ||
31 | + | ||
32 | + const pagination = reactive({ | ||
33 | + total: 10, | ||
34 | + current: 1, | ||
35 | + pageSize: 10, | ||
36 | + showQuickJumper: true, | ||
37 | + size: 'small', | ||
38 | + showTotal: (total: number) => `共 ${total} 条数据`, | ||
39 | + params: {} as Recordable, | ||
40 | + }); | ||
41 | + | ||
42 | + const dataSource = ref<TaskRecordType[]>([]); | ||
43 | + const loading = ref(false); | ||
44 | + | ||
45 | + const getDataSource = async () => { | ||
46 | + try { | ||
47 | + loading.value = true; | ||
48 | + const { items } = await getTaskCenterList({ | ||
49 | + page: pagination.current, | ||
50 | + pageSize: pagination.pageSize, | ||
51 | + ...pagination.params, | ||
52 | + }); | ||
53 | + dataSource.value = items; | ||
54 | + } catch (error) { | ||
55 | + throw error; | ||
56 | + } finally { | ||
57 | + loading.value = false; | ||
58 | + } | ||
59 | + }; | ||
60 | + | ||
61 | + const reload = () => getDataSource(); | ||
62 | + | ||
63 | + const setListHeight = () => { | ||
64 | + const clientHeight = document.documentElement.clientHeight; | ||
65 | + const rect = getBoundingClientRect(unref(listElRef)!.$el!) as DOMRect; | ||
66 | + // margin-top 24 height 24 | ||
67 | + const paginationHeight = 24 + 24 + 8; | ||
68 | + // list pading top 8 maring-top 8 extra slot 56 | ||
69 | + const listContainerMarginBottom = 8 + 8 + 56; | ||
70 | + const listContainerHeight = | ||
71 | + clientHeight - rect.top - paginationHeight - listContainerMarginBottom; | ||
72 | + const listContainerEl = (unref(listElRef)!.$el as HTMLElement).querySelector( | ||
73 | + '.ant-spin-container' | ||
74 | + ) as HTMLElement; | ||
75 | + listContainerEl && | ||
76 | + (listContainerEl.style.height = listContainerHeight + 'px') && | ||
77 | + (listContainerEl.style.overflowY = 'auto') && | ||
78 | + (listContainerEl.style.overflowX = 'hidden'); | ||
79 | + }; | ||
80 | + | ||
81 | + onMounted(() => { | ||
82 | + setListHeight(); | ||
83 | + getDataSource(); | ||
84 | + }); | ||
85 | +</script> | ||
86 | + | ||
87 | +<template> | ||
88 | + <PageWrapper class="bg-gray-100"> | ||
89 | + <section | ||
90 | + class="form-container bg-light-50 px-4 pt-4 mt-4 x dark:text-gray-300 dark:bg-dark-900" | ||
91 | + > | ||
92 | + <BasicForm @register="registerForm" /> | ||
93 | + </section> | ||
94 | + <section class="bg-light-50 my-4 p-4 x dark:text-gray-300 dark:bg-dark-900"> | ||
95 | + <List | ||
96 | + ref="listElRef" | ||
97 | + :dataSource="dataSource" | ||
98 | + :pagination="pagination" | ||
99 | + :grid="{ gutter: 16, xs: 1, sm: 1, md: 1, lg: 2, xl: 2, xxl: 3, column: 3 }" | ||
100 | + :loading="loading" | ||
101 | + > | ||
102 | + <template #header> | ||
103 | + <section class="flex justify-end gap-4"> | ||
104 | + <Tooltip title="刷新"> | ||
105 | + <Button type="primary" @click="getDataSource"> | ||
106 | + <ReloadOutlined :spin="loading" /> | ||
107 | + </Button> | ||
108 | + </Tooltip> | ||
109 | + </section> | ||
110 | + </template> | ||
111 | + <template #renderItem="{ item }"> | ||
112 | + <List.Item :key="item.id"> | ||
113 | + <TaskCard | ||
114 | + :record="item" | ||
115 | + :reload="reload" | ||
116 | + :tbDeviceId="tbDeviceId" | ||
117 | + :deviceTaskCardMode="true" | ||
118 | + /> | ||
119 | + </List.Item> | ||
120 | + </template> | ||
121 | + </List> | ||
122 | + </section> | ||
123 | + </PageWrapper> | ||
124 | +</template> |
1 | +import { Component } from 'vue'; | ||
2 | +import { ComponentType } from './type'; | ||
3 | + | ||
4 | +const componentMap = new Map<ComponentType, Component>(); | ||
5 | + | ||
6 | +export function add(compName: ComponentType, component: Component) { | ||
7 | + componentMap.set(compName, component); | ||
8 | +} | ||
9 | + | ||
10 | +export function del(compName: ComponentType) { | ||
11 | + componentMap.delete(compName); | ||
12 | +} | ||
13 | + | ||
14 | +export { componentMap }; |
1 | +import type { ComponentType } from './type'; | ||
2 | +import { tryOnUnmounted } from '@vueuse/core'; | ||
3 | +import { add, del } from './componentMap'; | ||
4 | +import type { Component } from 'vue'; | ||
5 | + | ||
6 | +export function useComponentRegister(compName: ComponentType, comp: Component) { | ||
7 | + add(compName, comp); | ||
8 | + tryOnUnmounted(() => { | ||
9 | + del(compName); | ||
10 | + }); | ||
11 | +} |
1 | +<script lang="ts" setup> | ||
2 | + import { BasicForm, FormSchema, useForm } from '/@/components/Form'; | ||
3 | + import { ComponentType, ColEx } from '/@/components/Form/src/types/index'; | ||
4 | + | ||
5 | + withDefaults( | ||
6 | + defineProps<{ | ||
7 | + component?: ComponentType; | ||
8 | + itemColProps?: Partial<ColEx>; | ||
9 | + }>(), | ||
10 | + { | ||
11 | + component: 'Switch', | ||
12 | + itemColProps: () => ({ span: 12 } as Partial<ColEx>), | ||
13 | + } | ||
14 | + ); | ||
15 | + | ||
16 | + const [registerForm, {}] = useForm({ | ||
17 | + showActionButtonGroup: false, | ||
18 | + schemas: Array.from({ length: 3 }).map((_item, index) => { | ||
19 | + return { | ||
20 | + field: index.toString(), | ||
21 | + label: index.toString(), | ||
22 | + component: 'Switch', | ||
23 | + } as FormSchema; | ||
24 | + }), | ||
25 | + // baseColProps, | ||
26 | + }); | ||
27 | +</script> | ||
28 | + | ||
29 | +<template> | ||
30 | + <BasicForm @register="registerForm" /> | ||
31 | +</template> |
1 | +export type ComponentType = ''; |
1 | +<script lang="ts" setup> | ||
2 | + import { Button } from 'ant-design-vue'; | ||
3 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
4 | + import { BasicModal, useModal } from '/@/components/Modal'; | ||
5 | + import { formSchemas } from './config'; | ||
6 | + import { ref } from 'vue'; | ||
7 | + import { unref } from 'vue'; | ||
8 | + | ||
9 | + const emit = defineEmits(['update:value']); | ||
10 | + | ||
11 | + const [registerModal] = useModal(); | ||
12 | + | ||
13 | + const [registerForm, {}] = useForm({ | ||
14 | + schemas: formSchemas, | ||
15 | + showActionButtonGroup: false, | ||
16 | + rowProps: { gutter: 10 }, | ||
17 | + baseColProps: { span: 12 }, | ||
18 | + }); | ||
19 | + | ||
20 | + const commandValue = ref(''); | ||
21 | + | ||
22 | + const handleGetValue = async () => {}; | ||
23 | + | ||
24 | + const handleOk = () => { | ||
25 | + emit('update:value', unref(commandValue)); | ||
26 | + }; | ||
27 | +</script> | ||
28 | + | ||
29 | +<template> | ||
30 | + <BasicModal @register="registerModal" title="配置操作" @ok="handleOk"> | ||
31 | + <BasicForm @register="registerForm" class="create-tcp-command-form" /> | ||
32 | + <section> | ||
33 | + <Button @click="handleGetValue" type="link" class="!px-0">生成预览</Button> | ||
34 | + <div v-if="commandValue"> | ||
35 | + <div class="text-gray-400">Modbus 指令预览</div> | ||
36 | + <code class="bg-dark-50 text-light-50 p-1 w-full block mt-1">{{ commandValue }}</code> | ||
37 | + </div> | ||
38 | + </section> | ||
39 | + </BasicModal> | ||
40 | +</template> | ||
41 | + | ||
42 | +<style lang="less" scoped> | ||
43 | + .create-tcp-command-form { | ||
44 | + :deep(.ant-input-number) { | ||
45 | + width: 100%; | ||
46 | + min-width: 0; | ||
47 | + } | ||
48 | + } | ||
49 | +</style> |
1 | +<script lang="ts" setup> | ||
2 | + import { InputGroup, InputNumber, Select, Input } from 'ant-design-vue'; | ||
3 | + import { watch } from 'vue'; | ||
4 | + import { unref } from 'vue'; | ||
5 | + import { computed } from 'vue'; | ||
6 | + import { ref } from 'vue'; | ||
7 | + | ||
8 | + enum AddressTypeEnum { | ||
9 | + DEC = 'DEC', | ||
10 | + HEX = 'HEX', | ||
11 | + } | ||
12 | + | ||
13 | + const emit = defineEmits(['update:value']); | ||
14 | + | ||
15 | + const DEC_MAX_VALUE = parseInt('0xffff', 16); | ||
16 | + | ||
17 | + const props = defineProps<{ | ||
18 | + value?: string; | ||
19 | + }>(); | ||
20 | + | ||
21 | + const addressTypeOptions = [ | ||
22 | + { label: AddressTypeEnum.DEC, value: AddressTypeEnum.DEC }, | ||
23 | + { label: AddressTypeEnum.HEX, value: AddressTypeEnum.HEX }, | ||
24 | + ]; | ||
25 | + | ||
26 | + const type = ref(AddressTypeEnum.DEC); | ||
27 | + | ||
28 | + const inputValue = ref<number | string>(0); | ||
29 | + | ||
30 | + const getHexValue = computed(() => { | ||
31 | + return parseInt(unref(inputValue) || 0, 16); | ||
32 | + }); | ||
33 | + | ||
34 | + const getDecValue = computed(() => { | ||
35 | + let formatValue = Number(unref(inputValue) || 0).toString(16); | ||
36 | + formatValue = `0x${formatValue.padStart(4, '0')}`; | ||
37 | + return (inputValue.value as number) > DEC_MAX_VALUE ? '0x0000' : formatValue; | ||
38 | + }); | ||
39 | + | ||
40 | + const handleEmit = () => { | ||
41 | + const syncValue = unref(type) === AddressTypeEnum.DEC ? unref(getDecValue) : unref(getHexValue); | ||
42 | + emit('update:value', syncValue); | ||
43 | + }; | ||
44 | + | ||
45 | + const handleChange = (value: AddressTypeEnum) => { | ||
46 | + const syncValue = value === AddressTypeEnum.DEC ? unref(getHexValue) : unref(getDecValue); | ||
47 | + inputValue.value = syncValue; | ||
48 | + emit('update:value', syncValue); | ||
49 | + }; | ||
50 | + | ||
51 | + watch( | ||
52 | + () => props.value, | ||
53 | + (targetValue) => { | ||
54 | + inputValue.value = targetValue || 0; | ||
55 | + console.log(`inputValue: ${unref(inputValue)}`); | ||
56 | + } | ||
57 | + ); | ||
58 | +</script> | ||
59 | + | ||
60 | +<template> | ||
61 | + <InputGroup compact class="!flex"> | ||
62 | + <Select | ||
63 | + v-model:value="type" | ||
64 | + :options="addressTypeOptions" | ||
65 | + @change="handleChange" | ||
66 | + class="bg-gray-200" | ||
67 | + /> | ||
68 | + <InputNumber | ||
69 | + v-if="type === AddressTypeEnum.DEC" | ||
70 | + v-model:value="inputValue" | ||
71 | + :step="1" | ||
72 | + class="flex-1" | ||
73 | + @change="handleEmit" | ||
74 | + /> | ||
75 | + <Input v-if="type === AddressTypeEnum.HEX" v-model:value="inputValue" /> | ||
76 | + <div class="text-center h-8 leading-8 px-2 bg-gray-200 cursor-pointer rounded-1 w-20"> | ||
77 | + <div v-if="type === AddressTypeEnum.DEC">{{ getDecValue }}</div> | ||
78 | + <div v-if="type === AddressTypeEnum.HEX">{{ getHexValue }}</div> | ||
79 | + </div> | ||
80 | + </InputGroup> | ||
81 | +</template> |
1 | +import { findDictItemByCode } from '/@/api/system/dict'; | ||
2 | +import { FormSchema, useComponentRegister } from '/@/components/Form'; | ||
3 | +import { DictEnum } from '/@/enums/dictEnum'; | ||
4 | +import RegisterAddressInput from './RegisterAddressInput.vue'; | ||
5 | +import { createPickerSearch } from '/@/utils/pickerSearch'; | ||
6 | +import { ControlGroup } from '../ControlGroup'; | ||
7 | + | ||
8 | +export enum FormFieldsEnum { | ||
9 | + ADDRESS = 'address', | ||
10 | + FUNCTION_CODE = 'functionCode', | ||
11 | + START_REGISTER_ADDRESS = 'startRegisterAddress', | ||
12 | + REGISTER_NUMBER = 'registerNumber', | ||
13 | + DATA_VALID = 'dataValid', | ||
14 | +} | ||
15 | + | ||
16 | +useComponentRegister('RegisterAddressInput', RegisterAddressInput); | ||
17 | +useComponentRegister('ControlGroup', ControlGroup); | ||
18 | + | ||
19 | +export const formSchemas: FormSchema[] = [ | ||
20 | + { | ||
21 | + field: FormFieldsEnum.ADDRESS, | ||
22 | + component: 'ApiSelect', | ||
23 | + label: '从机地址', | ||
24 | + componentProps: () => { | ||
25 | + return { | ||
26 | + api: async (params: Recordable) => { | ||
27 | + try { | ||
28 | + const result = await findDictItemByCode(params); | ||
29 | + return result.map((item, index) => ({ | ||
30 | + ...item, | ||
31 | + itemText: `${index + 1} - ${item.itemText}`, | ||
32 | + })); | ||
33 | + } catch (error) { | ||
34 | + return []; | ||
35 | + } | ||
36 | + }, | ||
37 | + params: { | ||
38 | + dictCode: DictEnum.SLAVE_ADDRESS, | ||
39 | + }, | ||
40 | + labelField: 'itemText', | ||
41 | + valueField: 'itemValue', | ||
42 | + ...createPickerSearch(), | ||
43 | + getPopupContainer: () => document.body, | ||
44 | + }; | ||
45 | + }, | ||
46 | + }, | ||
47 | + { | ||
48 | + field: FormFieldsEnum.FUNCTION_CODE, | ||
49 | + component: 'ApiSelect', | ||
50 | + label: '功能码', | ||
51 | + componentProps: () => { | ||
52 | + return { | ||
53 | + api: findDictItemByCode, | ||
54 | + params: { | ||
55 | + dictCode: DictEnum.FUNCTION_CODE, | ||
56 | + }, | ||
57 | + labelField: 'itemText', | ||
58 | + valueField: 'itemValue', | ||
59 | + getPopupContainer: () => document.body, | ||
60 | + }; | ||
61 | + }, | ||
62 | + }, | ||
63 | + { | ||
64 | + field: FormFieldsEnum.START_REGISTER_ADDRESS, | ||
65 | + label: '起始寄存器地址', | ||
66 | + component: 'RegisterAddressInput', | ||
67 | + valueField: 'value', | ||
68 | + changeEvent: 'update:value', | ||
69 | + }, | ||
70 | + { | ||
71 | + field: FormFieldsEnum.REGISTER_NUMBER, | ||
72 | + label: '寄存器个数', | ||
73 | + component: 'InputNumber', | ||
74 | + componentProps: { | ||
75 | + min: 1, | ||
76 | + max: 64, | ||
77 | + step: 1, | ||
78 | + }, | ||
79 | + }, | ||
80 | + { | ||
81 | + field: FormFieldsEnum.DATA_VALID, | ||
82 | + label: '数据校验', | ||
83 | + component: 'ApiSelect', | ||
84 | + colProps: { span: 24 }, | ||
85 | + componentProps: () => { | ||
86 | + return { | ||
87 | + api: findDictItemByCode, | ||
88 | + params: { | ||
89 | + dictCode: DictEnum.DATA_VALIDATE, | ||
90 | + }, | ||
91 | + labelField: 'itemText', | ||
92 | + valueField: 'itemValue', | ||
93 | + getPopupContainer: () => document.body, | ||
94 | + }; | ||
95 | + }, | ||
96 | + }, | ||
97 | + // { | ||
98 | + // field: 'test', | ||
99 | + // label: '住', | ||
100 | + // component: 'ControlGroup', | ||
101 | + // }, | ||
102 | +]; |
1 | +<script lang="ts" setup> | ||
2 | + import { Input } from 'ant-design-vue'; | ||
3 | + import { JSONEditor } from '/@/components/CodeEditor'; | ||
4 | + import { SettingOutlined } from '@ant-design/icons-vue'; | ||
5 | + import { ModeEnum } from './index'; | ||
6 | + import { computed } from '@vue/reactivity'; | ||
7 | + import CreateTCPCommandModal from './CreateTCPCommandModal.vue'; | ||
8 | + import { useModal } from '/@/components/Modal'; | ||
9 | + import { ref } from 'vue'; | ||
10 | + import { unref } from 'vue'; | ||
11 | + import JSONEditorType from 'jsoneditor'; | ||
12 | + | ||
13 | + const props = withDefaults( | ||
14 | + defineProps<{ | ||
15 | + value?: any; | ||
16 | + mode?: string; | ||
17 | + inputProps?: Recordable; | ||
18 | + showSettingAddonAfter?: boolean; | ||
19 | + openSettingOnInputFocus?: boolean; | ||
20 | + }>(), | ||
21 | + { | ||
22 | + mode: 'application/json', | ||
23 | + showSettingAddonAfter: true, | ||
24 | + openSettingOnInputFocus: false, | ||
25 | + } | ||
26 | + ); | ||
27 | + | ||
28 | + const emit = defineEmits(['update:value']); | ||
29 | + | ||
30 | + const inputElRef = ref<Nullable<HTMLInputElement>>(null); | ||
31 | + | ||
32 | + const [registerCreateTCPCommandModal, { openModal }] = useModal(); | ||
33 | + | ||
34 | + const getJSONValue = computed(() => { | ||
35 | + return props.value; | ||
36 | + }); | ||
37 | + | ||
38 | + const handleEmit = (value: any) => { | ||
39 | + emit('update:value', value); | ||
40 | + openModal(false); | ||
41 | + if (props.openSettingOnInputFocus) { | ||
42 | + unref(inputElRef)?.blur(); | ||
43 | + } | ||
44 | + }; | ||
45 | + | ||
46 | + const handleClick = () => { | ||
47 | + openModal(true); | ||
48 | + }; | ||
49 | + | ||
50 | + const handleFocus = () => { | ||
51 | + if (props.openSettingOnInputFocus) { | ||
52 | + openModal(true); | ||
53 | + unref(inputElRef)?.blur(); | ||
54 | + } | ||
55 | + }; | ||
56 | + | ||
57 | + const handleEditorBlur = (_event: Event, instance: JSONEditorType) => { | ||
58 | + const value = instance.getText(); | ||
59 | + handleEmit(value); | ||
60 | + }; | ||
61 | +</script> | ||
62 | + | ||
63 | +<template> | ||
64 | + <section> | ||
65 | + <Input | ||
66 | + v-if="mode === ModeEnum.NORMAL" | ||
67 | + ref="inputElRef" | ||
68 | + :value="getJSONValue" | ||
69 | + @change="handleEmit" | ||
70 | + v-bind="inputProps" | ||
71 | + @focus="handleFocus" | ||
72 | + > | ||
73 | + <template v-if="showSettingAddonAfter" #addonAfter> | ||
74 | + <SettingOutlined class="cursor-pointer" @click="handleClick" /> | ||
75 | + </template> | ||
76 | + </Input> | ||
77 | + <JSONEditor v-if="mode === ModeEnum.JSON" :value="getJSONValue" @blur="handleEditorBlur" /> | ||
78 | + <CreateTCPCommandModal @register="registerCreateTCPCommandModal" @update:value="handleEmit" /> | ||
79 | + </section> | ||
80 | +</template> |
1 | +import { TaskTargetEnum, TaskTargetNameEnum, TaskTypeEnum, TaskTypeNameEnum } from '../../config'; | ||
2 | +import { findDictItemByCode } from '/@/api/system/dict'; | ||
3 | +import { FormSchema, useComponentRegister } from '/@/components/Form'; | ||
4 | +import { | ||
5 | + DevicePicker, | ||
6 | + validateDevicePicker, | ||
7 | + FormFieldsEnum as DeviceCascadePickerFieldsEnum, | ||
8 | +} from '../DevicePicker'; | ||
9 | +import { CustomInput, ModeEnum } from '../CustomInput'; | ||
10 | +import { DeviceProfileModel } from '/@/api/device/model/deviceModel'; | ||
11 | +import { TransportTypeEnum } from '/@/views/device/profiles/components/TransportDescript/const'; | ||
12 | +import { getDeviceProfile } from '/@/api/alarm/position'; | ||
13 | +import { JSONEditorValidator } from '/@/components/CodeEditor/src/JSONEditor'; | ||
14 | +import { TimeUnitEnum, TimeUnitNameEnum } from '/@/enums/toolEnum'; | ||
15 | +import { createPickerSearch } from '/@/utils/pickerSearch'; | ||
16 | + | ||
17 | +useComponentRegister('DevicePicker', DevicePicker); | ||
18 | +useComponentRegister('CustomInput', CustomInput); | ||
19 | + | ||
20 | +export enum FormFieldsEnum { | ||
21 | + // 任务名称 | ||
22 | + NAME = 'name', | ||
23 | + // 目标类型 | ||
24 | + TARGET_TYPE = 'targetType', | ||
25 | + // 设备类型选择 | ||
26 | + DEVICE_PROFILE = 'deviceProfile', | ||
27 | + // 执行目标源 | ||
28 | + EXECUTE_TARGET_DATA = 'executeTargetData', | ||
29 | + // 执行任务类型 | ||
30 | + EXECUTE_CONTENT_TYPE = 'executeContentType', | ||
31 | + // 下发命令 | ||
32 | + RPC_COMMAND = 'rpcCommand', | ||
33 | + // 推送方式 | ||
34 | + PUSH_WAY = 'pushWay', | ||
35 | + // 执行周期类型 | ||
36 | + EXECUTE_TIME_TYPE = 'executeTimeType', | ||
37 | + // 执行间隔时间 | ||
38 | + EXECUTE_TIME_INTERVAL = 'interval', | ||
39 | + // 执行周期类型 | ||
40 | + EXECUTE_TIME_PERIOD_TYPE = 'periodType', | ||
41 | + // 周期 每月 每周 | ||
42 | + EXECUTE_TIME_PERIOD = 'period', | ||
43 | + // time时间 | ||
44 | + TIME = 'time', | ||
45 | + // 设备传输协议 | ||
46 | + TRANSPORT_TYPE = 'transportType', | ||
47 | + // 间隔时间单位 | ||
48 | + POLL_UNIT = 'pollUnit', | ||
49 | +} | ||
50 | + | ||
51 | +export enum PushWayEnum { | ||
52 | + MQTT = 'MQTT', | ||
53 | + TCP = 'TCP', | ||
54 | +} | ||
55 | + | ||
56 | +export enum ExecuteTimeTypeEnum { | ||
57 | + CUSTOM = 'CUSTOM', | ||
58 | + POLL = 'POLL', | ||
59 | +} | ||
60 | + | ||
61 | +export enum ExecuteTimeTypeNameEnum { | ||
62 | + CUSTOM = '自定义', | ||
63 | + POLL = '间隔时间重复', | ||
64 | +} | ||
65 | + | ||
66 | +export enum PeriodTypeEnum { | ||
67 | + MONTH, | ||
68 | + WEEK, | ||
69 | + DAY, | ||
70 | +} | ||
71 | + | ||
72 | +export enum PeriodTypeNameEnum { | ||
73 | + MONTH = '每月', | ||
74 | + WEEK = '每周', | ||
75 | + DAY = '每日', | ||
76 | +} | ||
77 | + | ||
78 | +const isShowCustomIntervalTimeSetting = (model: Recordable, flag: PeriodTypeEnum) => { | ||
79 | + return ( | ||
80 | + model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.CUSTOM && | ||
81 | + model[FormFieldsEnum.EXECUTE_TIME_PERIOD_TYPE] === flag | ||
82 | + ); | ||
83 | +}; | ||
84 | + | ||
85 | +export const formSchemas: FormSchema[] = [ | ||
86 | + { | ||
87 | + field: FormFieldsEnum.NAME, | ||
88 | + component: 'Input', | ||
89 | + label: '任务名称', | ||
90 | + rules: [{ required: true, message: '请填写任务名称' }], | ||
91 | + componentProps: { | ||
92 | + placeholder: '请输入任务名称', | ||
93 | + }, | ||
94 | + }, | ||
95 | + { | ||
96 | + field: FormFieldsEnum.TARGET_TYPE, | ||
97 | + component: 'RadioGroup', | ||
98 | + label: '目标类型', | ||
99 | + defaultValue: TaskTargetEnum.DEVICES, | ||
100 | + helpMessage: ['执行任务的目标设备,可以是多个指定的设备,也可以是一个设备类型下的所有设备.'], | ||
101 | + componentProps: { | ||
102 | + options: [ | ||
103 | + { label: TaskTargetNameEnum.DEVICES, value: TaskTargetEnum.DEVICES }, | ||
104 | + { label: TaskTargetNameEnum.PRODUCTS, value: TaskTargetEnum.PRODUCTS }, | ||
105 | + ], | ||
106 | + }, | ||
107 | + }, | ||
108 | + { | ||
109 | + field: FormFieldsEnum.DEVICE_PROFILE, | ||
110 | + component: 'ApiSelect', | ||
111 | + label: '产品', | ||
112 | + ifShow: ({ model }) => model[FormFieldsEnum.TARGET_TYPE] === TaskTargetEnum.PRODUCTS, | ||
113 | + componentProps: ({ formActionType }) => { | ||
114 | + const { setFieldsValue } = formActionType; | ||
115 | + return { | ||
116 | + api: getDeviceProfile, | ||
117 | + placeholder: '请选择产品', | ||
118 | + labelField: 'name', | ||
119 | + valueField: 'id', | ||
120 | + onChange(value: string, option: DeviceProfileModel) { | ||
121 | + if (value) { | ||
122 | + const isTCP = option.transportType === TransportTypeEnum.TCP; | ||
123 | + setFieldsValue({ | ||
124 | + [FormFieldsEnum.TRANSPORT_TYPE]: isTCP ? PushWayEnum.TCP : PushWayEnum.MQTT, | ||
125 | + [FormFieldsEnum.PUSH_WAY]: isTCP ? PushWayEnum.TCP : PushWayEnum.MQTT, | ||
126 | + ...(isTCP ? {} : { [FormFieldsEnum.EXECUTE_CONTENT_TYPE]: TaskTypeEnum.CUSTOM }), | ||
127 | + }); | ||
128 | + } | ||
129 | + }, | ||
130 | + ...createPickerSearch(), | ||
131 | + getPopupContainer: () => document.body, | ||
132 | + }; | ||
133 | + }, | ||
134 | + }, | ||
135 | + { | ||
136 | + field: FormFieldsEnum.EXECUTE_TARGET_DATA, | ||
137 | + component: 'DevicePicker', | ||
138 | + label: '设备', | ||
139 | + helpMessage: ['任务可以对目标设备按预设的时间策略执行任务。'], | ||
140 | + ifShow: ({ model }) => model[FormFieldsEnum.TARGET_TYPE] === TaskTargetEnum.DEVICES, | ||
141 | + rules: [validateDevicePicker()], | ||
142 | + valueField: 'value', | ||
143 | + changeEvent: 'update:value', | ||
144 | + componentProps: ({ formActionType }) => { | ||
145 | + const { setFieldsValue } = formActionType; | ||
146 | + return { | ||
147 | + multiple: true, | ||
148 | + onChange(key: string, _value: string | string[], option: DeviceProfileModel) { | ||
149 | + if (key === DeviceCascadePickerFieldsEnum.DEVICE_PROFILE) { | ||
150 | + const isTCP = (option || {}).transportType === TransportTypeEnum.TCP; | ||
151 | + setFieldsValue({ | ||
152 | + [FormFieldsEnum.TRANSPORT_TYPE]: _value ? option.transportType : null, | ||
153 | + [FormFieldsEnum.PUSH_WAY]: isTCP ? PushWayEnum.TCP : PushWayEnum.MQTT, | ||
154 | + ...(isTCP ? {} : { [FormFieldsEnum.EXECUTE_CONTENT_TYPE]: TaskTypeEnum.CUSTOM }), | ||
155 | + }); | ||
156 | + } | ||
157 | + }, | ||
158 | + }; | ||
159 | + }, | ||
160 | + }, | ||
161 | + { | ||
162 | + field: FormFieldsEnum.TRANSPORT_TYPE, | ||
163 | + component: 'Input', | ||
164 | + label: '', | ||
165 | + show: false, | ||
166 | + }, | ||
167 | + { | ||
168 | + field: FormFieldsEnum.EXECUTE_CONTENT_TYPE, | ||
169 | + component: 'RadioGroup', | ||
170 | + label: '任务类型', | ||
171 | + defaultValue: TaskTypeEnum.CUSTOM, | ||
172 | + componentProps: ({ formActionType, formModel }) => { | ||
173 | + const { setFieldsValue } = formActionType; | ||
174 | + const transportType = Reflect.get(formModel, FormFieldsEnum.TRANSPORT_TYPE); | ||
175 | + return { | ||
176 | + options: [ | ||
177 | + { | ||
178 | + label: TaskTypeNameEnum.CUSTOM, | ||
179 | + value: TaskTypeEnum.CUSTOM, | ||
180 | + }, | ||
181 | + { | ||
182 | + label: TaskTypeNameEnum.MODBUS_RTU, | ||
183 | + value: TaskTypeEnum.MODBUS_RTU, | ||
184 | + disabled: transportType && transportType !== PushWayEnum.TCP, | ||
185 | + }, | ||
186 | + ], | ||
187 | + onChange(value: TaskTypeEnum) { | ||
188 | + value && | ||
189 | + setFieldsValue({ | ||
190 | + [FormFieldsEnum.RPC_COMMAND]: '', | ||
191 | + }); | ||
192 | + }, | ||
193 | + }; | ||
194 | + }, | ||
195 | + }, | ||
196 | + { | ||
197 | + field: FormFieldsEnum.PUSH_WAY, | ||
198 | + component: 'RadioGroup', | ||
199 | + label: '推送方式', | ||
200 | + defaultValue: PushWayEnum.MQTT, | ||
201 | + show: false, | ||
202 | + componentProps: { | ||
203 | + options: [ | ||
204 | + { label: PushWayEnum.MQTT, value: PushWayEnum.MQTT }, | ||
205 | + { label: PushWayEnum.TCP, value: PushWayEnum.TCP }, | ||
206 | + ], | ||
207 | + }, | ||
208 | + }, | ||
209 | + { | ||
210 | + field: FormFieldsEnum.RPC_COMMAND, | ||
211 | + component: 'CustomInput', | ||
212 | + label: '自定义数据流', | ||
213 | + rules: [{ required: true, message: '请输入自定义数据流' }], | ||
214 | + dynamicRules: ({ model }) => | ||
215 | + model[FormFieldsEnum.PUSH_WAY] === PushWayEnum.MQTT ? JSONEditorValidator() : [], | ||
216 | + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_CONTENT_TYPE] === TaskTypeEnum.CUSTOM, | ||
217 | + valueField: 'value', | ||
218 | + changeEvent: 'update:value', | ||
219 | + componentProps: ({ formModel }) => { | ||
220 | + const pushMode = Reflect.get(formModel, FormFieldsEnum.PUSH_WAY); | ||
221 | + return { | ||
222 | + inputProps: { | ||
223 | + placeholder: '请输入自定义数据流', | ||
224 | + }, | ||
225 | + mode: pushMode === PushWayEnum.MQTT ? ModeEnum.JSON : ModeEnum.NORMAL, | ||
226 | + }; | ||
227 | + }, | ||
228 | + }, | ||
229 | + { | ||
230 | + field: FormFieldsEnum.RPC_COMMAND, | ||
231 | + component: 'CustomInput', | ||
232 | + label: 'ModbusRTU轮询', | ||
233 | + rules: [{ required: true, message: '请输入Modbus RTU 轮询指令' }], | ||
234 | + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_CONTENT_TYPE] === TaskTypeEnum.MODBUS_RTU, | ||
235 | + valueField: 'value', | ||
236 | + changeEvent: 'update:value', | ||
237 | + componentProps: () => { | ||
238 | + return { | ||
239 | + inputProps: { | ||
240 | + placeholder: '请输入Modbus RTU 轮询指令', | ||
241 | + }, | ||
242 | + showSettingAddonAfter: false, | ||
243 | + openSettingOnInputFocus: true, | ||
244 | + mode: ModeEnum.NORMAL, | ||
245 | + }; | ||
246 | + }, | ||
247 | + }, | ||
248 | + { | ||
249 | + field: FormFieldsEnum.EXECUTE_TIME_TYPE, | ||
250 | + label: '任务定时设置', | ||
251 | + component: 'RadioGroup', | ||
252 | + defaultValue: ExecuteTimeTypeEnum.POLL, | ||
253 | + componentProps: { | ||
254 | + options: [ | ||
255 | + { label: ExecuteTimeTypeNameEnum.POLL, value: ExecuteTimeTypeEnum.POLL }, | ||
256 | + { label: ExecuteTimeTypeNameEnum.CUSTOM, value: ExecuteTimeTypeEnum.CUSTOM }, | ||
257 | + ], | ||
258 | + }, | ||
259 | + }, | ||
260 | + { | ||
261 | + field: FormFieldsEnum.EXECUTE_TIME_PERIOD_TYPE, | ||
262 | + component: 'Select', | ||
263 | + label: '周期', | ||
264 | + required: true, | ||
265 | + defaultValue: PeriodTypeEnum.MONTH, | ||
266 | + componentProps: { | ||
267 | + placeholder: '请选择周期', | ||
268 | + options: [ | ||
269 | + { label: PeriodTypeNameEnum.DAY, value: PeriodTypeEnum.DAY }, | ||
270 | + { label: PeriodTypeNameEnum.WEEK, value: PeriodTypeEnum.WEEK }, | ||
271 | + { label: PeriodTypeNameEnum.MONTH, value: PeriodTypeEnum.MONTH }, | ||
272 | + ], | ||
273 | + getPopupContainer: () => document.body, | ||
274 | + }, | ||
275 | + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.CUSTOM, | ||
276 | + }, | ||
277 | + { | ||
278 | + field: FormFieldsEnum.EXECUTE_TIME_PERIOD, | ||
279 | + component: 'ApiSelect', | ||
280 | + label: '每月', | ||
281 | + required: true, | ||
282 | + componentProps: { | ||
283 | + placeholder: '请选择月份', | ||
284 | + api: findDictItemByCode, | ||
285 | + params: { | ||
286 | + dictCode: 'every_month', | ||
287 | + }, | ||
288 | + labelField: 'itemText', | ||
289 | + valueField: 'itemValue', | ||
290 | + getPopupContainer: () => document.body, | ||
291 | + }, | ||
292 | + ifShow: ({ model }) => isShowCustomIntervalTimeSetting(model, PeriodTypeEnum.MONTH), | ||
293 | + }, | ||
294 | + { | ||
295 | + field: FormFieldsEnum.EXECUTE_TIME_PERIOD, | ||
296 | + component: 'ApiSelect', | ||
297 | + label: '每周', | ||
298 | + required: true, | ||
299 | + componentProps: { | ||
300 | + placeholder: '请选择周期', | ||
301 | + api: findDictItemByCode, | ||
302 | + params: { | ||
303 | + dictCode: 'every_week', | ||
304 | + }, | ||
305 | + labelField: 'itemText', | ||
306 | + valueField: 'itemValue', | ||
307 | + getPopupContainer: () => document.body, | ||
308 | + }, | ||
309 | + ifShow: ({ model }) => isShowCustomIntervalTimeSetting(model, PeriodTypeEnum.WEEK), | ||
310 | + }, | ||
311 | + { | ||
312 | + field: FormFieldsEnum.TIME, | ||
313 | + component: 'TimePicker', | ||
314 | + label: '时间', | ||
315 | + required: true, | ||
316 | + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.CUSTOM, | ||
317 | + componentProps: { | ||
318 | + getPopupContainer: () => document.body, | ||
319 | + valueFormat: 'HH:mm:ss', | ||
320 | + }, | ||
321 | + }, | ||
322 | + { | ||
323 | + field: FormFieldsEnum.POLL_UNIT, | ||
324 | + component: 'RadioGroup', | ||
325 | + label: '时间单位', | ||
326 | + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.POLL, | ||
327 | + defaultValue: TimeUnitEnum.SECOND, | ||
328 | + componentProps: { | ||
329 | + options: [ | ||
330 | + { label: TimeUnitNameEnum.SECOND, value: TimeUnitEnum.SECOND }, | ||
331 | + { label: TimeUnitNameEnum.MINUTE, value: TimeUnitEnum.MINUTE }, | ||
332 | + { label: TimeUnitNameEnum.HOUR, value: TimeUnitEnum.HOUR }, | ||
333 | + ], | ||
334 | + }, | ||
335 | + }, | ||
336 | + { | ||
337 | + field: FormFieldsEnum.EXECUTE_TIME_INTERVAL, | ||
338 | + label: '间隔时间', | ||
339 | + component: 'InputNumber', | ||
340 | + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.POLL, | ||
341 | + componentProps: ({ formModel }) => { | ||
342 | + const unit = formModel[FormFieldsEnum.POLL_UNIT]; | ||
343 | + return { | ||
344 | + min: 0, | ||
345 | + max: unit === TimeUnitEnum.HOUR ? 23 : 59, | ||
346 | + step: 1, | ||
347 | + placeholder: '请输入间隔时间', | ||
348 | + }; | ||
349 | + }, | ||
350 | + }, | ||
351 | +]; |
1 | +export { default as DetailModal } from './index.vue'; |
1 | +<script lang="ts" setup> | ||
2 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
3 | + import { BasicModal, useModalInner } from '/@/components/Modal'; | ||
4 | + import { formSchemas } from './config'; | ||
5 | + import { ref } from 'vue'; | ||
6 | + import { composeData, parseData } from './util'; | ||
7 | + import { createTask, updateTask } from '/@/api/task'; | ||
8 | + import { ModalParamsType } from '/#/utils'; | ||
9 | + import { DataActionModeEnum } from '/@/enums/toolEnum'; | ||
10 | + import { TaskRecordType } from '/@/api/task/model'; | ||
11 | + import { FormValueType } from './util'; | ||
12 | + import { unref } from 'vue'; | ||
13 | + | ||
14 | + const props = defineProps<{ | ||
15 | + reload: Fn; | ||
16 | + }>(); | ||
17 | + defineEmits(['register']); | ||
18 | + | ||
19 | + const formMode = ref(DataActionModeEnum.CREATE); | ||
20 | + | ||
21 | + const dataSource = ref<TaskRecordType>(); | ||
22 | + const [registerModal, { closeModal }] = useModalInner( | ||
23 | + ( | ||
24 | + { record, mode }: ModalParamsType<TaskRecordType> = { | ||
25 | + record: {} as unknown as TaskRecordType, | ||
26 | + mode: DataActionModeEnum.CREATE, | ||
27 | + } | ||
28 | + ) => { | ||
29 | + dataSource.value = record; | ||
30 | + formMode.value = mode; | ||
31 | + resetFields(); | ||
32 | + if (record && mode === DataActionModeEnum.UPDATE) { | ||
33 | + const res = parseData(record); | ||
34 | + console.log({ record, res }); | ||
35 | + setFieldsValue({ ...res }); | ||
36 | + } | ||
37 | + } | ||
38 | + ); | ||
39 | + | ||
40 | + const [registerForm, { getFieldsValue, validate, setFieldsValue, resetFields }] = useForm({ | ||
41 | + schemas: formSchemas, | ||
42 | + showActionButtonGroup: false, | ||
43 | + layout: 'inline', | ||
44 | + baseColProps: { span: 24 }, | ||
45 | + labelWidth: 140, | ||
46 | + }); | ||
47 | + | ||
48 | + const loading = ref(false); | ||
49 | + const handleOk = async () => { | ||
50 | + try { | ||
51 | + loading.value = true; | ||
52 | + await validate(); | ||
53 | + const res = getFieldsValue(); | ||
54 | + const _res = composeData(res as Required<FormValueType>); | ||
55 | + formMode.value === DataActionModeEnum.CREATE | ||
56 | + ? await createTask(_res) | ||
57 | + : await updateTask({ ..._res, id: unref(dataSource)?.id as string }); | ||
58 | + closeModal(); | ||
59 | + props.reload?.(); | ||
60 | + } catch (error) { | ||
61 | + throw error; | ||
62 | + } finally { | ||
63 | + loading.value = false; | ||
64 | + } | ||
65 | + }; | ||
66 | +</script> | ||
67 | + | ||
68 | +<template> | ||
69 | + <BasicModal | ||
70 | + @register="registerModal" | ||
71 | + title="创建任务" | ||
72 | + width="700px" | ||
73 | + :okButtonProps="{ loading }" | ||
74 | + @ok="handleOk" | ||
75 | + > | ||
76 | + <BasicForm @register="registerForm" class="form-container" /> | ||
77 | + </BasicModal> | ||
78 | +</template> | ||
79 | + | ||
80 | +<style lang="less" scoped> | ||
81 | + .form-container { | ||
82 | + :deep(.ant-input-number) { | ||
83 | + width: 100%; | ||
84 | + } | ||
85 | + } | ||
86 | +</style> |
1 | +import CronParser, { CronFields, HourRange, SixtyRange } from 'cron-parser'; | ||
2 | +import { dateUtil } from '/@/utils/dateUtil'; | ||
3 | +import { ExecuteTimeTypeEnum, FormFieldsEnum, PushWayEnum } from './config'; | ||
4 | +import { CreateTaskRecordType, TaskRecordType } from '/@/api/task/model'; | ||
5 | +import { DeviceCascadePickerValueType } from '../DevicePicker'; | ||
6 | +import { TaskTargetEnum } from '../../config'; | ||
7 | +import { TimeUnitEnum } from '/@/enums/toolEnum'; | ||
8 | + | ||
9 | +export interface FormValueType | ||
10 | + extends Partial<Record<Exclude<FormFieldsEnum, FormFieldsEnum.TRANSPORT_TYPE>, any>> { | ||
11 | + [FormFieldsEnum.EXECUTE_TARGET_DATA]: DeviceCascadePickerValueType; | ||
12 | +} | ||
13 | + | ||
14 | +type CanWrite<T> = { | ||
15 | + -readonly [K in keyof T]: T[K]; | ||
16 | +}; | ||
17 | + | ||
18 | +interface GenCronExpressionResultType { | ||
19 | + effective: boolean; | ||
20 | + expression?: string; | ||
21 | +} | ||
22 | + | ||
23 | +export const usePluginGenCronExpression = ( | ||
24 | + time: string, | ||
25 | + expression = '* * * * * * *', | ||
26 | + includesYear = true | ||
27 | +): GenCronExpressionResultType => { | ||
28 | + try { | ||
29 | + const separator = ' '; | ||
30 | + const removeYear = expression.split(separator).slice(0, 6).join(separator); | ||
31 | + | ||
32 | + const date = dateUtil(time, 'HH:mm:ss'); | ||
33 | + | ||
34 | + const second = date.get('second') as SixtyRange; | ||
35 | + const minute = date.get('minute') as SixtyRange; | ||
36 | + const hour = date.get('hour') as HourRange; | ||
37 | + | ||
38 | + const result = CronParser.parseExpression(removeYear, { utc: true, nthDayOfWeek: 4 }); | ||
39 | + const fields = JSON.parse(JSON.stringify(result.fields)) as CanWrite<CronFields>; | ||
40 | + fields.second = [second]; | ||
41 | + fields.minute = [minute]; | ||
42 | + fields.hour = [hour]; | ||
43 | + | ||
44 | + // console.log(CronParser.fieldsToExpression(CronParser.parseExpression('').fields).stringify()); | ||
45 | + | ||
46 | + let newExpression = CronParser.fieldsToExpression(fields).stringify(true); | ||
47 | + newExpression = includesYear ? `${newExpression} *` : newExpression; | ||
48 | + return { effective: true, expression: newExpression }; | ||
49 | + } catch (error) { | ||
50 | + // throw error; | ||
51 | + return { effective: false }; | ||
52 | + } | ||
53 | +}; | ||
54 | + | ||
55 | +export const genCronExpression = ( | ||
56 | + time: string, | ||
57 | + expression = '* * * * * * *' | ||
58 | +): GenCronExpressionResultType => { | ||
59 | + try { | ||
60 | + const separator = ' '; | ||
61 | + const list: (string | number)[] = expression.split(separator); | ||
62 | + | ||
63 | + const date = dateUtil(time, 'HH:mm:ss'); | ||
64 | + | ||
65 | + const second = date.get('second') as SixtyRange; | ||
66 | + const minute = date.get('minute') as SixtyRange; | ||
67 | + const hour = date.get('hour') as HourRange; | ||
68 | + | ||
69 | + list[0] = second; | ||
70 | + list[1] = minute; | ||
71 | + list[2] = hour; | ||
72 | + | ||
73 | + return { effective: true, expression: list.join(separator) }; | ||
74 | + } catch (error) { | ||
75 | + // throw error; | ||
76 | + return { effective: false }; | ||
77 | + } | ||
78 | +}; | ||
79 | + | ||
80 | +export const composeData = (result: Required<FormValueType>): CreateTaskRecordType => { | ||
81 | + const { | ||
82 | + name, | ||
83 | + targetType, | ||
84 | + rpcCommand, | ||
85 | + pushWay, | ||
86 | + executeContentType, | ||
87 | + executeTargetData, | ||
88 | + deviceProfile, | ||
89 | + executeTimeType, | ||
90 | + period, | ||
91 | + periodType, | ||
92 | + time, | ||
93 | + interval, | ||
94 | + pollUnit, | ||
95 | + } = result; | ||
96 | + | ||
97 | + const { organizationId, deviceType, deviceId, deviceProfileId } = executeTargetData || {}; | ||
98 | + | ||
99 | + const { expression } = genCronExpression(time, period); | ||
100 | + | ||
101 | + const cron = | ||
102 | + executeTimeType === ExecuteTimeTypeEnum.POLL ? `0/${interval} * * * * ? *` : expression!; | ||
103 | + | ||
104 | + return { | ||
105 | + name, | ||
106 | + targetType, | ||
107 | + executeContent: { | ||
108 | + pushContent: { | ||
109 | + rpcCommand: pushWay === PushWayEnum.MQTT ? JSON.parse(rpcCommand) : pushWay, | ||
110 | + }, | ||
111 | + pushWay, | ||
112 | + type: executeContentType, | ||
113 | + }, | ||
114 | + executeTarget: { | ||
115 | + data: targetType === TaskTargetEnum.PRODUCTS ? [deviceProfile] : (deviceId as string[]), | ||
116 | + deviceType, | ||
117 | + organizationId, | ||
118 | + deviceProfileId: targetType === TaskTargetEnum.PRODUCTS ? deviceProfile : deviceProfileId, | ||
119 | + }, | ||
120 | + executeTime: { | ||
121 | + type: executeTimeType, | ||
122 | + periodType, | ||
123 | + period, | ||
124 | + time: executeTimeType === ExecuteTimeTypeEnum.POLL ? interval : time, | ||
125 | + cron, | ||
126 | + pollUnit, | ||
127 | + }, | ||
128 | + }; | ||
129 | +}; | ||
130 | + | ||
131 | +export const parseData = (result: TaskRecordType): Required<FormValueType> => { | ||
132 | + const { name, targetType, executeContent, executeTarget, executeTime } = result; | ||
133 | + const { pushContent: { rpcCommand } = {}, pushWay, type: executeContentType } = executeContent; | ||
134 | + const { data, deviceProfileId, deviceType, organizationId } = executeTarget as Required< | ||
135 | + TaskRecordType['executeTarget'] | ||
136 | + >; | ||
137 | + const { type: executeTimeType, period, periodType, time, pollUnit } = executeTime; | ||
138 | + return { | ||
139 | + name, | ||
140 | + targetType, | ||
141 | + rpcCommand: pushWay === PushWayEnum.MQTT ? JSON.stringify(rpcCommand) : rpcCommand, | ||
142 | + pushWay, | ||
143 | + executeContentType, | ||
144 | + executeTargetData: { | ||
145 | + deviceId: targetType === TaskTargetEnum.DEVICES ? data : [], | ||
146 | + deviceProfileId, | ||
147 | + deviceType, | ||
148 | + organizationId, | ||
149 | + }, | ||
150 | + deviceProfile: targetType === TaskTargetEnum.PRODUCTS ? data : null, | ||
151 | + executeTimeType, | ||
152 | + period, | ||
153 | + periodType: executeTimeType === ExecuteTimeTypeEnum.CUSTOM ? periodType : null, | ||
154 | + time: executeTimeType === ExecuteTimeTypeEnum.CUSTOM ? time : null, | ||
155 | + interval: executeTimeType === ExecuteTimeTypeEnum.POLL ? time : null, | ||
156 | + pollUnit: executeTimeType === ExecuteTimeTypeEnum.POLL ? pollUnit : TimeUnitEnum.SECOND, | ||
157 | + }; | ||
158 | +}; |
1 | +export { default as DevicePicker } from './index.vue'; | ||
2 | +export { validateDevicePicker } from './utils'; | ||
3 | + | ||
4 | +export enum FormFieldsEnum { | ||
5 | + DEVICE_TYPE = 'deviceType', | ||
6 | + DEVICE_PROFILE = 'deviceProfileId', | ||
7 | + ORGANIZATION = 'organizationId', | ||
8 | + DEVICE = 'deviceId', | ||
9 | +} | ||
10 | + | ||
11 | +export interface DeviceCascadePickerValueType | ||
12 | + extends Record<Exclude<FormFieldsEnum, FormFieldsEnum.DEVICE>, string> { | ||
13 | + [FormFieldsEnum.DEVICE]: string | string[]; | ||
14 | +} |
1 | +<script lang="ts" setup> | ||
2 | + import { h } from 'vue'; | ||
3 | + import { getMeetTheConditionsDevice } from '/@/api/dataBoard'; | ||
4 | + import { getOrganizationList } from '/@/api/system/system'; | ||
5 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
6 | + import { copyTransFun } from '/@/utils/fnUtils'; | ||
7 | + import { Tooltip } from 'ant-design-vue'; | ||
8 | + import { watch } from 'vue'; | ||
9 | + import { nextTick } from 'vue'; | ||
10 | + import { getDeviceProfile } from '/@/api/alarm/position'; | ||
11 | + import { findDictItemByCode } from '/@/api/system/dict'; | ||
12 | + import { DictEnum } from '/@/enums/dictEnum'; | ||
13 | + import { FormFieldsEnum } from '.'; | ||
14 | + import { isObject } from '/@/utils/is'; | ||
15 | + import { createPickerSearch } from '/@/utils/pickerSearch'; | ||
16 | + | ||
17 | + const props = withDefaults( | ||
18 | + defineProps<{ | ||
19 | + value?: Recordable; | ||
20 | + multiple?: boolean; | ||
21 | + max?: 3; | ||
22 | + }>(), | ||
23 | + { | ||
24 | + value: () => ({}), | ||
25 | + multiple: false, | ||
26 | + } | ||
27 | + ); | ||
28 | + | ||
29 | + const emit = defineEmits(['update:value', 'change']); | ||
30 | + | ||
31 | + const handleChange = (key: string, value: string | string[], option: Recordable) => { | ||
32 | + emit('change', key, value, option); | ||
33 | + }; | ||
34 | + | ||
35 | + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars | ||
36 | + const createMaxTagPlaceholder = (omittedValues: Record<'label' | 'value', string>[]) => { | ||
37 | + return h( | ||
38 | + Tooltip, | ||
39 | + {}, | ||
40 | + { | ||
41 | + default: () => h('span', `+${omittedValues.length}...`), | ||
42 | + title: () => h('span', 'other'), | ||
43 | + } | ||
44 | + ); | ||
45 | + }; | ||
46 | + | ||
47 | + const handleEmit = async (key: string, value: string | string[], option: Recordable) => { | ||
48 | + await nextTick(); | ||
49 | + let _value = getFieldsValue(); | ||
50 | + _value = { | ||
51 | + ..._value, | ||
52 | + | ||
53 | + ...(key === FormFieldsEnum.DEVICE_TYPE | ||
54 | + ? { | ||
55 | + [FormFieldsEnum.DEVICE_PROFILE]: null, | ||
56 | + [FormFieldsEnum.ORGANIZATION]: null, | ||
57 | + [FormFieldsEnum.DEVICE]: props.multiple ? [] : null, | ||
58 | + } | ||
59 | + : {}), | ||
60 | + | ||
61 | + ...(key === FormFieldsEnum.DEVICE_PROFILE | ||
62 | + ? { | ||
63 | + [FormFieldsEnum.ORGANIZATION]: null, | ||
64 | + [FormFieldsEnum.DEVICE]: props.multiple ? [] : null, | ||
65 | + } | ||
66 | + : {}), | ||
67 | + | ||
68 | + ...(key === FormFieldsEnum.ORGANIZATION | ||
69 | + ? { | ||
70 | + [FormFieldsEnum.DEVICE]: props.multiple ? [] : null, | ||
71 | + } | ||
72 | + : {}), | ||
73 | + }; | ||
74 | + handleChange(key, value, option); | ||
75 | + emit('update:value', { ..._value }); | ||
76 | + }; | ||
77 | + const [register, { setFieldsValue, getFieldsValue, resetFields }] = useForm({ | ||
78 | + schemas: [ | ||
79 | + { | ||
80 | + field: FormFieldsEnum.DEVICE_TYPE, | ||
81 | + component: 'ApiSelect', | ||
82 | + label: '', | ||
83 | + componentProps: () => { | ||
84 | + return { | ||
85 | + api: findDictItemByCode, | ||
86 | + params: { | ||
87 | + dictCode: DictEnum.DEVICE_TYPE, | ||
88 | + }, | ||
89 | + labelField: 'itemText', | ||
90 | + valueField: 'itemValue', | ||
91 | + placeholder: '请选择设备类型', | ||
92 | + onChange(value: string, option: Recordable) { | ||
93 | + handleEmit(FormFieldsEnum.DEVICE_TYPE, value, option); | ||
94 | + }, | ||
95 | + getPopupContainer: () => document.body, | ||
96 | + ...createPickerSearch(), | ||
97 | + }; | ||
98 | + }, | ||
99 | + }, | ||
100 | + { | ||
101 | + field: FormFieldsEnum.DEVICE_PROFILE, | ||
102 | + component: 'ApiSelect', | ||
103 | + label: '', | ||
104 | + componentProps: ({ formModel }) => { | ||
105 | + const deviceType = Reflect.get(formModel, FormFieldsEnum.DEVICE_TYPE); | ||
106 | + return { | ||
107 | + api: async () => { | ||
108 | + try { | ||
109 | + return await getDeviceProfile(deviceType); | ||
110 | + } catch (error) { | ||
111 | + return []; | ||
112 | + } | ||
113 | + }, | ||
114 | + placeholder: '请选择产品', | ||
115 | + labelField: 'name', | ||
116 | + valueField: 'id', | ||
117 | + onChange(value: string, options: Recordable) { | ||
118 | + handleEmit(FormFieldsEnum.DEVICE_PROFILE, value, options); | ||
119 | + }, | ||
120 | + getPopupContainer: () => document.body, | ||
121 | + ...createPickerSearch(), | ||
122 | + }; | ||
123 | + }, | ||
124 | + }, | ||
125 | + { | ||
126 | + field: FormFieldsEnum.ORGANIZATION, | ||
127 | + component: 'ApiTreeSelect', | ||
128 | + label: '', | ||
129 | + componentProps: () => { | ||
130 | + return { | ||
131 | + api: async () => { | ||
132 | + try { | ||
133 | + const data = await getOrganizationList(); | ||
134 | + copyTransFun(data as any); | ||
135 | + return data; | ||
136 | + } catch (error) { | ||
137 | + console.log(error); | ||
138 | + return []; | ||
139 | + } | ||
140 | + }, | ||
141 | + placeholder: '请选择组织', | ||
142 | + labelField: 'name', | ||
143 | + valueField: 'id', | ||
144 | + childField: 'children', | ||
145 | + onChange(value: string, option: Recordable) { | ||
146 | + handleEmit(FormFieldsEnum.ORGANIZATION, value, option); | ||
147 | + }, | ||
148 | + getPopupContainer: () => document.body, | ||
149 | + }; | ||
150 | + }, | ||
151 | + }, | ||
152 | + { | ||
153 | + field: FormFieldsEnum.DEVICE, | ||
154 | + label: '', | ||
155 | + component: 'ApiSelect', | ||
156 | + componentProps: ({ formModel }) => { | ||
157 | + const deviceType = Reflect.get(formModel, FormFieldsEnum.DEVICE_TYPE); | ||
158 | + const deviceProfileId = Reflect.get(formModel, FormFieldsEnum.DEVICE_PROFILE); | ||
159 | + const organizationId = Reflect.get(formModel, FormFieldsEnum.ORGANIZATION); | ||
160 | + return { | ||
161 | + api: async () => { | ||
162 | + try { | ||
163 | + if (!organizationId) return []; | ||
164 | + return await getMeetTheConditionsDevice({ | ||
165 | + deviceProfileId, | ||
166 | + deviceType, | ||
167 | + organizationId, | ||
168 | + }); | ||
169 | + } catch (error) { | ||
170 | + return []; | ||
171 | + } | ||
172 | + }, | ||
173 | + mode: props.multiple ? 'multiple' : 'combobox', | ||
174 | + labelField: 'name', | ||
175 | + valueField: 'tbDeviceId', | ||
176 | + placeholder: '请选择设备', | ||
177 | + maxTagCount: 3, | ||
178 | + maxTagTextLength: 4, | ||
179 | + onChange(value: string, option: Recordable) { | ||
180 | + handleEmit(FormFieldsEnum.DEVICE, value, option); | ||
181 | + }, | ||
182 | + getPopupContainer: () => document.body, | ||
183 | + ...createPickerSearch(), | ||
184 | + }; | ||
185 | + }, | ||
186 | + }, | ||
187 | + ], | ||
188 | + showActionButtonGroup: false, | ||
189 | + layout: 'inline', | ||
190 | + baseColProps: { span: 6 }, | ||
191 | + }); | ||
192 | + | ||
193 | + const setValue = async () => { | ||
194 | + await nextTick(); | ||
195 | + if (!props.value || (isObject(props.value) && !Object.values(props.value).length)) { | ||
196 | + resetFields(); | ||
197 | + return; | ||
198 | + } | ||
199 | + setFieldsValue(props.value); | ||
200 | + }; | ||
201 | + | ||
202 | + watch( | ||
203 | + () => props.value, | ||
204 | + () => { | ||
205 | + setValue(); | ||
206 | + }, | ||
207 | + { | ||
208 | + immediate: true, | ||
209 | + } | ||
210 | + ); | ||
211 | +</script> | ||
212 | + | ||
213 | +<template> | ||
214 | + <BasicForm @register="register" class="device-picker" /> | ||
215 | +</template> | ||
216 | + | ||
217 | +<style lang="less" scoped> | ||
218 | + .device-picker { | ||
219 | + :deep(.ant-row) { | ||
220 | + width: 100%; | ||
221 | + } | ||
222 | + | ||
223 | + :deep(.ant-form-item-control-input-content) { | ||
224 | + div > div { | ||
225 | + width: 100%; | ||
226 | + } | ||
227 | + } | ||
228 | + } | ||
229 | +</style> |
1 | +import { FormFieldsEnum } from '.'; | ||
2 | +import { Rule } from '/@/components/Form'; | ||
3 | + | ||
4 | +export const validateDevicePicker = () => { | ||
5 | + return { | ||
6 | + required: true, | ||
7 | + validateTrigger: 'blur', | ||
8 | + validator(_rule: Recordable, value: Recordable, _callback: Fn) { | ||
9 | + const device = Reflect.get(value || {}, FormFieldsEnum.DEVICE); | ||
10 | + if (!device) return Promise.reject('请选择设备'); | ||
11 | + return Promise.resolve(); | ||
12 | + }, | ||
13 | + } as Rule; | ||
14 | +}; |
1 | +export { default as TaskCard } from './index.vue'; |
1 | +<script lang="ts" setup> | ||
2 | + import { Button, Card, Switch, Tooltip } from 'ant-design-vue'; | ||
3 | + import { | ||
4 | + CloudSyncOutlined, | ||
5 | + PlayCircleOutlined, | ||
6 | + QuestionCircleOutlined, | ||
7 | + } from '@ant-design/icons-vue'; | ||
8 | + import { TaskRecordType } from '/@/api/task/model'; | ||
9 | + import { StateEnum, TaskTargetNameEnum, TaskTypeEnum } from '../../config'; | ||
10 | + import { TaskTypeNameEnum } from '../../config'; | ||
11 | + import AuthDropDown from '/@/components/Widget/AuthDropDown.vue'; | ||
12 | + import { useMessage } from '/@/hooks/web/useMessage'; | ||
13 | + import { cancelTask, deleteTask, updateState } from '/@/api/task'; | ||
14 | + import { computed } from '@vue/reactivity'; | ||
15 | + import { unref } from 'vue'; | ||
16 | + import { ref } from 'vue'; | ||
17 | + | ||
18 | + enum DropMenuEvent { | ||
19 | + DELETE = 'DELETE', | ||
20 | + EDIT = 'EDIT', | ||
21 | + } | ||
22 | + | ||
23 | + const props = withDefaults( | ||
24 | + defineProps<{ | ||
25 | + record: TaskRecordType; | ||
26 | + reload: Fn; | ||
27 | + deviceTaskCardMode?: boolean; | ||
28 | + tbDeviceId?: string; | ||
29 | + }>(), | ||
30 | + { | ||
31 | + deviceTaskCardMode: false, | ||
32 | + } | ||
33 | + ); | ||
34 | + | ||
35 | + const emit = defineEmits(['edit']); | ||
36 | + | ||
37 | + const loading = ref(false); | ||
38 | + | ||
39 | + const { createMessage } = useMessage(); | ||
40 | + | ||
41 | + const getRecord = computed(() => { | ||
42 | + return props.record; | ||
43 | + }); | ||
44 | + | ||
45 | + const getCancelState = computed(() => { | ||
46 | + const { executeTarget } = unref(getRecord); | ||
47 | + const { cancelExecuteDevices } = executeTarget; | ||
48 | + return !!(cancelExecuteDevices && cancelExecuteDevices.length); | ||
49 | + }); | ||
50 | + | ||
51 | + const handleDelete = async () => { | ||
52 | + try { | ||
53 | + await deleteTask([unref(getRecord).id]); | ||
54 | + createMessage.success('删除成功'); | ||
55 | + props.reload?.(); | ||
56 | + } catch (error) {} | ||
57 | + }; | ||
58 | + | ||
59 | + const handleSwitchState = async () => { | ||
60 | + try { | ||
61 | + loading.value = true; | ||
62 | + await updateState( | ||
63 | + unref(getRecord).id, | ||
64 | + !unref(getRecord).state ? StateEnum.ENABLE : StateEnum.CLOSE | ||
65 | + ); | ||
66 | + createMessage.success('更新状态成功'); | ||
67 | + props.reload?.(); | ||
68 | + } catch (error) { | ||
69 | + throw error; | ||
70 | + } finally { | ||
71 | + loading.value = false; | ||
72 | + } | ||
73 | + }; | ||
74 | + | ||
75 | + const handleCancelTask = async () => { | ||
76 | + try { | ||
77 | + if (!props.tbDeviceId) return; | ||
78 | + loading.value = true; | ||
79 | + await cancelTask({ id: unref(getRecord).id, tbDeviceId: props.tbDeviceId }); | ||
80 | + createMessage.success('设置成功'); | ||
81 | + props.reload?.(); | ||
82 | + } catch (error) { | ||
83 | + throw error; | ||
84 | + } finally { | ||
85 | + loading.value = false; | ||
86 | + } | ||
87 | + }; | ||
88 | +</script> | ||
89 | + | ||
90 | +<template> | ||
91 | + <Card hoverable class="card-container !rounded"> | ||
92 | + <div v-if="!deviceTaskCardMode" class="flex justify-end mb-4"> | ||
93 | + <AuthDropDown | ||
94 | + @click.stop | ||
95 | + :trigger="['hover']" | ||
96 | + :drop-menu-list="[ | ||
97 | + { | ||
98 | + text: '编辑', | ||
99 | + event: DropMenuEvent.DELETE, | ||
100 | + icon: 'ant-design:edit-outlined', | ||
101 | + onClick: emit.bind(null, 'edit', getRecord), | ||
102 | + }, | ||
103 | + { | ||
104 | + text: '删除', | ||
105 | + event: DropMenuEvent.DELETE, | ||
106 | + icon: 'ant-design:delete-outlined', | ||
107 | + popconfirm: { | ||
108 | + title: '是否确认删除操作?', | ||
109 | + onConfirm: handleDelete.bind(null), | ||
110 | + }, | ||
111 | + }, | ||
112 | + ]" | ||
113 | + /> | ||
114 | + </div> | ||
115 | + <div class="flex text-base font-medium justify-between mb-2"> | ||
116 | + <Tooltip :title="getRecord.name" placement="topLeft"> | ||
117 | + <div class="truncate max-w-48">{{ getRecord.name }}</div> | ||
118 | + </Tooltip> | ||
119 | + <span | ||
120 | + v-if="deviceTaskCardMode" | ||
121 | + class="text-xs w-12 leading-6 text-right" | ||
122 | + :class="getRecord.state === StateEnum.ENABLE ? 'text-green-500' : 'text-red-500'" | ||
123 | + > | ||
124 | + {{ getRecord.state === StateEnum.ENABLE ? '已启用' : '未启用' }} | ||
125 | + </span> | ||
126 | + <Switch | ||
127 | + v-if="!deviceTaskCardMode" | ||
128 | + :checked="getRecord.state === StateEnum.ENABLE" | ||
129 | + :loading="loading" | ||
130 | + size="small" | ||
131 | + @click="handleSwitchState" | ||
132 | + /> | ||
133 | + </div> | ||
134 | + <div class="flex gap-2 items-center"> | ||
135 | + <div | ||
136 | + class="rounded flex justify-center items-center w-6 h-6" | ||
137 | + :class=" | ||
138 | + getRecord.executeContent.type === TaskTypeEnum.MODBUS_RTU | ||
139 | + ? ' bg-blue-400' | ||
140 | + : 'bg-green-400' | ||
141 | + " | ||
142 | + > | ||
143 | + <CloudSyncOutlined class="svg:fill-light-50" /> | ||
144 | + </div> | ||
145 | + <div class="text-gray-400 truncate"> | ||
146 | + <span>{{ TaskTypeNameEnum[getRecord.executeContent.type] }}</span> | ||
147 | + <span class="mx-1">-</span> | ||
148 | + <span>{{ TaskTargetNameEnum[getRecord.targetType] }}</span> | ||
149 | + </div> | ||
150 | + </div> | ||
151 | + <div class="mt-4 flex justify-between items-center gap-3"> | ||
152 | + <Button size="small"> | ||
153 | + <div class="text-xs px-1"> | ||
154 | + <PlayCircleOutlined class="mr-1" /> | ||
155 | + <span>运行任务</span> | ||
156 | + </div> | ||
157 | + </Button> | ||
158 | + <Tooltip title="最后运行时间:"> | ||
159 | + <div class="text-gray-400 text-xs truncate"> | ||
160 | + <span class="mr-2">间隔时间重复</span> | ||
161 | + <span>三分钟前</span> | ||
162 | + </div> | ||
163 | + </Tooltip> | ||
164 | + </div> | ||
165 | + | ||
166 | + <section | ||
167 | + v-if="deviceTaskCardMode" | ||
168 | + class="border-t mt-4 pt-2 text-sm border-gray-100 flex justify-between text-gray-400" | ||
169 | + > | ||
170 | + <div> | ||
171 | + <span>允许该设备</span> | ||
172 | + <Tooltip title="设置是否允许当前设备定时执行任务。该选项不影响手动执行任务。"> | ||
173 | + <QuestionCircleOutlined class="ml-1" /> | ||
174 | + </Tooltip> | ||
175 | + </div> | ||
176 | + <div> | ||
177 | + <Switch | ||
178 | + size="small" | ||
179 | + :loading="loading" | ||
180 | + :checked="getCancelState" | ||
181 | + @click="handleCancelTask" | ||
182 | + /> | ||
183 | + </div> | ||
184 | + </section> | ||
185 | + </Card> | ||
186 | +</template> |
src/views/task/center/config/index.ts
0 → 100644
1 | +import { FormSchema } from '/@/components/Form'; | ||
2 | + | ||
3 | +export enum FormFieldsEnum { | ||
4 | + DEVICE_TYPE = 'targetType', | ||
5 | + TASK_TYPE = 'taskType', | ||
6 | + TASK_STATUS = 'state', | ||
7 | +} | ||
8 | + | ||
9 | +export enum TaskTargetEnum { | ||
10 | + DEVICES = 'DEVICES', | ||
11 | + PRODUCTS = 'PRODUCTS', | ||
12 | + ALL = 'all', | ||
13 | +} | ||
14 | + | ||
15 | +export enum TaskTargetNameEnum { | ||
16 | + DEVICES = '设备', | ||
17 | + PRODUCTS = '产品', | ||
18 | + ALL = '不限任务目标类型', | ||
19 | +} | ||
20 | + | ||
21 | +export enum TaskTypeEnum { | ||
22 | + CUSTOM = 'CUSTOM', | ||
23 | + MODBUS_RTU = 'MODBUS_RTU', | ||
24 | +} | ||
25 | + | ||
26 | +export enum TaskTypeNameEnum { | ||
27 | + MODBUS_RTU = 'MODBUS_RTU轮询', | ||
28 | + CUSTOM = '自定义数据下发', | ||
29 | +} | ||
30 | + | ||
31 | +export enum TaskStatusEnum { | ||
32 | + DEACTIVATE, | ||
33 | + NORMAL, | ||
34 | +} | ||
35 | + | ||
36 | +export enum TaskStatusNameEnum { | ||
37 | + DEACTIVATE = '已停用', | ||
38 | + NORMAL = '正常', | ||
39 | +} | ||
40 | + | ||
41 | +export const formSchemas: FormSchema[] = [ | ||
42 | + { | ||
43 | + label: '目标类型', | ||
44 | + component: 'Select', | ||
45 | + field: FormFieldsEnum.DEVICE_TYPE, | ||
46 | + componentProps: { | ||
47 | + options: [ | ||
48 | + { label: TaskTargetNameEnum.DEVICES, value: TaskTargetEnum.DEVICES }, | ||
49 | + { label: TaskTargetNameEnum.PRODUCTS, value: TaskTargetEnum.PRODUCTS }, | ||
50 | + ], | ||
51 | + placeholder: '请选择目标类型', | ||
52 | + getPopupContainer: () => document.body, | ||
53 | + }, | ||
54 | + }, | ||
55 | + // { | ||
56 | + // label: '', | ||
57 | + // component: 'Select', | ||
58 | + // field: FormFieldsEnum.TASK_TYPE, | ||
59 | + // defaultValue: TaskTypeEnum.ALL, | ||
60 | + // componentProps: { | ||
61 | + // options: [ | ||
62 | + // { label: TaskTypeNameEnum.CUSTOM, value: TaskTypeEnum.CUSTOM }, | ||
63 | + // { label: TaskTypeNameEnum.MODBUS_RTU, value: TaskTypeEnum.MODBUS_RTU }, | ||
64 | + // ], | ||
65 | + // getPopupContainer: () => document.body, | ||
66 | + // }, | ||
67 | + // }, | ||
68 | + { | ||
69 | + label: '状态', | ||
70 | + component: 'Select', | ||
71 | + field: FormFieldsEnum.TASK_STATUS, | ||
72 | + componentProps: { | ||
73 | + options: [ | ||
74 | + { label: TaskStatusNameEnum.NORMAL, value: TaskStatusEnum.NORMAL }, | ||
75 | + { label: TaskStatusNameEnum.DEACTIVATE, value: TaskStatusEnum.DEACTIVATE }, | ||
76 | + ], | ||
77 | + placeholder: '请选择状态', | ||
78 | + getPopupContainer: () => document.body, | ||
79 | + }, | ||
80 | + }, | ||
81 | +]; | ||
82 | + | ||
83 | +export enum PermissionEnum { | ||
84 | + DELETE = 'delete', | ||
85 | +} | ||
86 | + | ||
87 | +export enum StateEnum { | ||
88 | + CLOSE, | ||
89 | + ENABLE, | ||
90 | +} |
src/views/task/center/index.vue
0 → 100644
1 | +<script setup lang="ts"> | ||
2 | + import { Button, List, Tooltip } from 'ant-design-vue'; | ||
3 | + import { PageWrapper } from '/@/components/Page'; | ||
4 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
5 | + import { formSchemas } from './config'; | ||
6 | + import { TaskCard } from './components/TaskCard'; | ||
7 | + import { getTaskCenterList } from '/@/api/task'; | ||
8 | + import { ref } from 'vue'; | ||
9 | + import { onMounted } from 'vue'; | ||
10 | + import { DetailModal } from './components/DetailModal'; | ||
11 | + import { useModal } from '/@/components/Modal'; | ||
12 | + import { TaskRecordType } from '/@/api/task/model'; | ||
13 | + import { reactive } from 'vue'; | ||
14 | + import { ReloadOutlined } from '@ant-design/icons-vue'; | ||
15 | + import { DataActionModeEnum } from '/@/enums/toolEnum'; | ||
16 | + import { ModalParamsType } from '/#/utils'; | ||
17 | + | ||
18 | + const [registerModal, { openModal }] = useModal(); | ||
19 | + | ||
20 | + const [registerForm, { getFieldsValue }] = useForm({ | ||
21 | + schemas: formSchemas, | ||
22 | + baseColProps: { span: 8 }, | ||
23 | + compact: true, | ||
24 | + showAdvancedButton: true, | ||
25 | + labelWidth: 100, | ||
26 | + submitFunc: async () => { | ||
27 | + pagination.params = getFieldsValue(); | ||
28 | + getDataSource(); | ||
29 | + }, | ||
30 | + }); | ||
31 | + | ||
32 | + const pagination = reactive({ | ||
33 | + total: 10, | ||
34 | + current: 1, | ||
35 | + showQuickJumper: true, | ||
36 | + size: 'small', | ||
37 | + showTotal: (total: number) => `共 ${total} 条数据`, | ||
38 | + params: {} as Recordable, | ||
39 | + }); | ||
40 | + | ||
41 | + const dataSource = ref<TaskRecordType[]>([]); | ||
42 | + const loading = ref(false); | ||
43 | + const getDataSource = async () => { | ||
44 | + try { | ||
45 | + loading.value = true; | ||
46 | + const { items } = await getTaskCenterList({ page: 1, pageSize: 10, ...pagination.params }); | ||
47 | + dataSource.value = items; | ||
48 | + } catch (error) { | ||
49 | + throw error; | ||
50 | + } finally { | ||
51 | + loading.value = false; | ||
52 | + } | ||
53 | + }; | ||
54 | + | ||
55 | + const handleEdit = (record: TaskRecordType) => { | ||
56 | + openModal(true, { | ||
57 | + record, | ||
58 | + mode: DataActionModeEnum.UPDATE, | ||
59 | + } as ModalParamsType); | ||
60 | + }; | ||
61 | + | ||
62 | + const reload = () => getDataSource(); | ||
63 | + | ||
64 | + onMounted(() => { | ||
65 | + getDataSource(); | ||
66 | + }); | ||
67 | +</script> | ||
68 | + | ||
69 | +<template> | ||
70 | + <PageWrapper class="container"> | ||
71 | + <section | ||
72 | + class="bg-light-50 flex p-4 justify-between items-center x dark:text-gray-300 dark:bg-dark-900" | ||
73 | + > | ||
74 | + <div class="text-2xl">任务</div> | ||
75 | + <Button | ||
76 | + type="primary" | ||
77 | + @click="openModal(true, { mode: DataActionModeEnum.CREATE } as ModalParamsType)" | ||
78 | + >创建任务</Button | ||
79 | + > | ||
80 | + </section> | ||
81 | + <section | ||
82 | + class="form-container bg-light-50 px-4 pt-4 mt-4 x dark:text-gray-300 dark:bg-dark-900" | ||
83 | + > | ||
84 | + <BasicForm @register="registerForm" /> | ||
85 | + </section> | ||
86 | + <section class="bg-light-50 mt-4 p-4 x dark:text-gray-300 dark:bg-dark-900"> | ||
87 | + <List | ||
88 | + :dataSource="dataSource" | ||
89 | + :pagination="pagination" | ||
90 | + :grid="{ gutter: 16, xs: 1, sm: 1, md: 2, lg: 3, xl: 3, xxl: 4, column: 4 }" | ||
91 | + :loading="loading" | ||
92 | + > | ||
93 | + <template #header> | ||
94 | + <section class="flex justify-end gap-4"> | ||
95 | + <!-- <CardLayoutButton /> --> | ||
96 | + <Tooltip title="刷新"> | ||
97 | + <Button type="primary" @click="getDataSource"> | ||
98 | + <ReloadOutlined :spin="loading" /> | ||
99 | + </Button> | ||
100 | + </Tooltip> | ||
101 | + </section> | ||
102 | + </template> | ||
103 | + <template #renderItem="{ item }"> | ||
104 | + <List.Item :key="item.id"> | ||
105 | + <TaskCard :record="item" :reload="reload" @edit="handleEdit" /> | ||
106 | + </List.Item> | ||
107 | + </template> | ||
108 | + </List> | ||
109 | + </section> | ||
110 | + <DetailModal @register="registerModal" :reload="reload" /> | ||
111 | + </PageWrapper> | ||
112 | +</template> | ||
113 | + | ||
114 | +<style lang="less" scoped></style> |
1 | import type { ComputedRef, Ref } from 'vue'; | 1 | import type { ComputedRef, Ref } from 'vue'; |
2 | +import { DataActionModeEnum } from '/@/enums/toolEnum'; | ||
2 | 3 | ||
3 | export type DynamicProps<T> = { | 4 | export type DynamicProps<T> = { |
4 | [P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>; | 5 | [P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>; |
5 | }; | 6 | }; |
7 | + | ||
8 | +export interface ModalParamsType<T = Recordable> { | ||
9 | + mode: DataActionModeEnum; | ||
10 | + record: T; | ||
11 | +} |