Showing
34 changed files
with
2043 additions
and
1 deletions
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 | 2 | // @ts-ignore |
3 | 3 | import codeEditor from './src/CodeEditor.vue'; |
4 | 4 | import jsonPreview from './src/json-preview/JsonPreview.vue'; |
5 | +export { JSONEditor } from './src/JSONEditor'; | |
5 | 6 | |
6 | 7 | export const CodeEditor = withInstall(codeEditor); |
7 | 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 | 122 | | 'ApiSelectScrollLoad' |
123 | 123 | | 'TransferModal' |
124 | 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 | 51 | <TabPane key="eventManage" tab="事件管理"> |
52 | 52 | <EventManage :tbDeviceId="deviceDetail.tbDeviceId" /> |
53 | 53 | </TabPane> |
54 | + <TabPane key="task" tab="任务"> | |
55 | + <Task :tbDeviceId="deviceDetail.tbDeviceId" /> | |
56 | + </TabPane> | |
54 | 57 | </Tabs> |
55 | 58 | </BasicDrawer> |
56 | 59 | </template> |
... | ... | @@ -71,6 +74,7 @@ |
71 | 74 | import ModelOfMatter from '../tabs/ModelOfMatter.vue'; |
72 | 75 | import EventManage from '../tabs/EventManage/index.vue'; |
73 | 76 | import { DeviceRecord } from '/@/api/device/model/deviceModel'; |
77 | + import Task from '../tabs/Task.vue'; | |
74 | 78 | |
75 | 79 | export default defineComponent({ |
76 | 80 | name: 'DeviceModal', |
... | ... | @@ -88,6 +92,7 @@ |
88 | 92 | ModelOfMatter, |
89 | 93 | CommandRecord, |
90 | 94 | EventManage, |
95 | + Task, | |
91 | 96 | }, |
92 | 97 | emits: ['reload', 'register', 'openTbDeviceDetail', 'openGatewayDeviceDetail'], |
93 | 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 | 1 | import type { ComputedRef, Ref } from 'vue'; |
2 | +import { DataActionModeEnum } from '/@/enums/toolEnum'; | |
2 | 3 | |
3 | 4 | export type DynamicProps<T> = { |
4 | 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 | +} | ... | ... |