Commit cbb36837759d57efcd0c5004afb3044819d3ab53
Merge remote-tracking branch 'origin/ww'
# Conflicts: # .env.development
Showing
52 changed files
with
3196 additions
and
89 deletions
1 | +import { DeviceRecord } from '../device/model/deviceModel'; | |
1 | 2 | import { |
2 | 3 | AddDataBoardParams, |
3 | 4 | AddDataComponentParams, |
... | ... | @@ -183,7 +184,7 @@ export const getAllDeviceByOrg = (organizationId: string, deviceProfileId?: stri |
183 | 184 | * @returns |
184 | 185 | */ |
185 | 186 | export const getMeetTheConditionsDevice = (params: GetMeetTheConditionsDeviceParams) => { |
186 | - return defHttp.get({ | |
187 | + return defHttp.get<DeviceRecord[]>({ | |
187 | 188 | url: DeviceUrl.GET_DEVICE, |
188 | 189 | params, |
189 | 190 | }); | ... | ... |
... | ... | @@ -40,6 +40,11 @@ enum DeviceManagerApi { |
40 | 40 | DEVICE_PUBLIC = '/customer/public/device', |
41 | 41 | |
42 | 42 | DEVICE_PRIVATE = '/customer/device', |
43 | + | |
44 | + /** | |
45 | + * @description 通过设备列表获取设备信息 | |
46 | + */ | |
47 | + QUERY_DEVICES = '/device/get/devices', | |
43 | 48 | } |
44 | 49 | |
45 | 50 | export const devicePage = (params: DeviceQueryParam) => { |
... | ... | @@ -330,3 +335,10 @@ export const privateDevice = (tbDeviceId: string) => { |
330 | 335 | { joinPrefix: false } |
331 | 336 | ); |
332 | 337 | }; |
338 | + | |
339 | +export const getDevicesByDeviceIds = (ids: string[]) => { | |
340 | + return defHttp.post<Record<'data', DeviceModel[]>>({ | |
341 | + url: DeviceManagerApi.QUERY_DEVICES, | |
342 | + data: ids, | |
343 | + }); | |
344 | +}; | ... | ... |
src/api/task/index.ts
0 → 100644
1 | +import { | |
2 | + CreateTaskRecordType, | |
3 | + GenModbusCommandType, | |
4 | + GetTaskListParamsType, | |
5 | + ImmediateExecuteTaskType, | |
6 | + TaskRecordType, | |
7 | +} from './model'; | |
8 | +import { PaginationResult } from '/#/axios'; | |
9 | +import { defHttp } from '/@/utils/http/axios'; | |
10 | + | |
11 | +enum Api { | |
12 | + TASK_LIST = '/task_center', | |
13 | + ADD_TASK = '/task_center/add', | |
14 | + UPDATE_STATE = '/task_center', | |
15 | + DELETE_TASK = '/task_center', | |
16 | + UPDATE_TASK = '/task_center/update', | |
17 | + CANCEL_TASK = '/task_center', | |
18 | + | |
19 | + GEN_MODBUS_COMMAND = '/js/modbus', | |
20 | + | |
21 | + IMMEDIATE_EXECUTE = '/task_center/immediate/execute', | |
22 | +} | |
23 | + | |
24 | +export const getTaskCenterList = (params: GetTaskListParamsType) => { | |
25 | + return defHttp.get<PaginationResult<TaskRecordType>>({ | |
26 | + url: Api.TASK_LIST, | |
27 | + params, | |
28 | + }); | |
29 | +}; | |
30 | + | |
31 | +export const createTask = (data: CreateTaskRecordType) => { | |
32 | + return defHttp.post({ | |
33 | + url: Api.ADD_TASK, | |
34 | + data, | |
35 | + }); | |
36 | +}; | |
37 | + | |
38 | +export const updateState = (id: string, state: number) => { | |
39 | + return defHttp.put({ | |
40 | + url: `${Api.UPDATE_STATE}/${id}/update/${state}`, | |
41 | + }); | |
42 | +}; | |
43 | + | |
44 | +export const deleteTask = (ids: string[]) => { | |
45 | + return defHttp.delete({ | |
46 | + url: Api.DELETE_TASK, | |
47 | + data: { ids }, | |
48 | + }); | |
49 | +}; | |
50 | + | |
51 | +export const updateTask = (data: CreateTaskRecordType & Record<'id', string>) => { | |
52 | + return defHttp.put({ | |
53 | + url: Api.UPDATE_TASK, | |
54 | + data, | |
55 | + }); | |
56 | +}; | |
57 | + | |
58 | +/** | |
59 | + * @description 取消任务 | |
60 | + * @param data | |
61 | + * @returns | |
62 | + */ | |
63 | +export const cancelTask = ( | |
64 | + data: Record<'id' | 'tbDeviceId', string> & Record<'allow', boolean> | |
65 | +) => { | |
66 | + return defHttp.put({ | |
67 | + url: `${Api.CANCEL_TASK}/${data.id}/update/${data.tbDeviceId}/${data.allow}`, | |
68 | + }); | |
69 | +}; | |
70 | + | |
71 | +/** | |
72 | + * @description 生成modbus指令 | |
73 | + * @param data | |
74 | + * @returns {string} | |
75 | + */ | |
76 | +export const genModbusCommand = (data: GenModbusCommandType) => { | |
77 | + return defHttp.post<string>({ | |
78 | + url: Api.GEN_MODBUS_COMMAND, | |
79 | + data, | |
80 | + }); | |
81 | +}; | |
82 | + | |
83 | +export const immediateExecute = (data: ImmediateExecuteTaskType) => { | |
84 | + return defHttp.post<Record<'data', boolean>>( | |
85 | + { | |
86 | + url: Api.IMMEDIATE_EXECUTE, | |
87 | + params: data, | |
88 | + }, | |
89 | + { joinParamsToUrl: true } | |
90 | + ); | |
91 | +}; | ... | ... |
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/DetailModal/config'; | |
7 | +import { TaskTypeEnum } from '/@/views/task/center/config'; | |
8 | + | |
9 | +export interface GetTaskListParamsType { | |
10 | + page: number; | |
11 | + pageSize: number; | |
12 | + state?: string; | |
13 | + tbDeviceId?: string; | |
14 | +} | |
15 | + | |
16 | +export interface CreateTaskRecordType { | |
17 | + name: string; | |
18 | + targetType: TaskTargetEnum; | |
19 | + executeTarget: { | |
20 | + organizationId?: string; | |
21 | + deviceProfileId?: string; | |
22 | + deviceType?: string; | |
23 | + cancelExecuteDevices?: string[]; | |
24 | + data?: string[]; | |
25 | + }; | |
26 | + executeContent: { | |
27 | + pushContent: { | |
28 | + rpcCommand: string | Recordable; | |
29 | + }; | |
30 | + pushWay: PushWayEnum; | |
31 | + type: TaskTypeEnum; | |
32 | + }; | |
33 | + executeTime: { | |
34 | + cron: string; | |
35 | + type: ExecuteTimeTypeEnum; | |
36 | + periodType: PeriodTypeEnum; | |
37 | + period: string; | |
38 | + time: string; | |
39 | + pollUnit: string; | |
40 | + }; | |
41 | +} | |
42 | + | |
43 | +export interface TaskRecordType extends CreateTaskRecordType { | |
44 | + id: string; | |
45 | + createTime: string; | |
46 | + creator: string; | |
47 | + enabled: boolean; | |
48 | + state: number; | |
49 | + lastExecuteTime?: number; | |
50 | + tkDeviceTaskCenter?: { | |
51 | + allowState: number; | |
52 | + taskCenterId: string; | |
53 | + tbDeviceId: string; | |
54 | + }; | |
55 | +} | |
56 | + | |
57 | +export interface GenModbusCommandType { | |
58 | + crc: string; | |
59 | + deviceCode: string; | |
60 | + method: string; | |
61 | + registerAddr: string; | |
62 | + registerNum?: number; | |
63 | + registerValues?: number[]; | |
64 | +} | |
65 | + | |
66 | +export interface ImmediateExecuteTaskType { | |
67 | + executeTarget: TaskTargetEnum; | |
68 | + id: string; | |
69 | + cronExpression: string; | |
70 | + targetIds: string[]; | |
71 | + name: string; | |
72 | +} | ... | ... |
... | ... | @@ -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 | + | |
10 | + enum EventEnum { | |
11 | + UPDATE_VALUE = 'update:value', | |
12 | + CHANGE = 'change', | |
13 | + BLUR = 'blur', | |
14 | + FOCUS = 'focus', | |
15 | + } | |
16 | + | |
17 | + const props = withDefaults( | |
18 | + defineProps<{ | |
19 | + value?: string; | |
20 | + options?: JSONEditorOptions; | |
21 | + }>(), | |
22 | + { | |
23 | + options: () => | |
24 | + ({ | |
25 | + mode: 'code', | |
26 | + mainMenuBar: false, | |
27 | + statusBar: false, | |
28 | + } as JSONEditorOptions), | |
29 | + } | |
30 | + ); | |
31 | + | |
32 | + const emit = defineEmits<{ | |
33 | + (e: EventEnum.UPDATE_VALUE, value: any, instance?: JSONEditor): void; | |
34 | + (e: EventEnum.CHANGE, value: any, instance?: JSONEditor): void; | |
35 | + (e: EventEnum.BLUR, event: Event, instance?: JSONEditor): void; | |
36 | + (e: EventEnum.FOCUS, event: Event, instance?: JSONEditor): void; | |
37 | + }>(); | |
38 | + | |
39 | + const jsonEditorElRef = ref<Nullable<any>>(); | |
40 | + | |
41 | + const editoreRef = ref<JSONEditor>(); | |
42 | + | |
43 | + const handleChange = (value: any) => { | |
44 | + emit(EventEnum.UPDATE_VALUE, value, unref(editoreRef)); | |
45 | + emit(EventEnum.CHANGE, value, unref(editoreRef)); | |
46 | + }; | |
47 | + | |
48 | + const handleEmit = (event: Event, key: EventEnum) => { | |
49 | + emit(key as EventEnum[keyof EventEnum], event, unref(editoreRef)); | |
50 | + }; | |
51 | + | |
52 | + const getOptions = computed(() => { | |
53 | + const { options } = props; | |
54 | + return { | |
55 | + ...options, | |
56 | + onChangeText: handleChange, | |
57 | + onBlur: (event: Event) => handleEmit(event, EventEnum.BLUR), | |
58 | + onFocus: (event: Event) => handleEmit(event, EventEnum.FOCUS), | |
59 | + } as JSONEditorOptions; | |
60 | + }); | |
61 | + | |
62 | + const initialize = () => { | |
63 | + editoreRef.value = new JSONEditor(unref(jsonEditorElRef), unref(getOptions)); | |
64 | + }; | |
65 | + | |
66 | + // watch( | |
67 | + // () => props.value, | |
68 | + // (target) => { | |
69 | + // unref(editoreRef)?.setText(target || ''); | |
70 | + // }, | |
71 | + // { | |
72 | + // immediate: true, | |
73 | + // } | |
74 | + // ); | |
75 | + | |
76 | + const get = (): string => { | |
77 | + return unref(editoreRef)?.getText() || ''; | |
78 | + }; | |
79 | + | |
80 | + const set = (data: any) => { | |
81 | + return unref(editoreRef)?.set(data); | |
82 | + }; | |
83 | + | |
84 | + onMounted(() => { | |
85 | + initialize(); | |
86 | + unref(editoreRef)?.setText(props.value || ''); | |
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,9 @@ export type ComponentType = |
122 | 122 | | 'ApiSelectScrollLoad' |
123 | 123 | | 'TransferModal' |
124 | 124 | | 'TransferTableModal' |
125 | - | 'ObjectModelValidateForm'; | |
125 | + | 'ObjectModelValidateForm' | |
126 | + | 'DevicePicker' | |
127 | + | 'ProductPicker' | |
128 | + | 'PollCommandInput' | |
129 | + | 'RegisterAddressInput' | |
130 | + | '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 | + const props = 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 | + tbDeviceId: props.tbDeviceId, | |
52 | + ...pagination.params, | |
53 | + }); | |
54 | + dataSource.value = items; | |
55 | + } catch (error) { | |
56 | + throw error; | |
57 | + } finally { | |
58 | + loading.value = false; | |
59 | + } | |
60 | + }; | |
61 | + | |
62 | + const reload = () => getDataSource(); | |
63 | + | |
64 | + const setListHeight = () => { | |
65 | + const clientHeight = document.documentElement.clientHeight; | |
66 | + const rect = getBoundingClientRect(unref(listElRef)!.$el!) as DOMRect; | |
67 | + // margin-top 24 height 24 | |
68 | + const paginationHeight = 24 + 24 + 8; | |
69 | + // list pading top 8 maring-top 8 extra slot 56 | |
70 | + const listContainerMarginBottom = 8 + 8 + 72; | |
71 | + const listContainerHeight = | |
72 | + clientHeight - rect.top - paginationHeight - listContainerMarginBottom; | |
73 | + const listContainerEl = (unref(listElRef)!.$el as HTMLElement).querySelector( | |
74 | + '.ant-spin-container' | |
75 | + ) as HTMLElement; | |
76 | + listContainerEl && | |
77 | + (listContainerEl.style.height = listContainerHeight + 'px') && | |
78 | + (listContainerEl.style.overflowY = 'auto') && | |
79 | + (listContainerEl.style.overflowX = 'hidden'); | |
80 | + }; | |
81 | + | |
82 | + onMounted(() => { | |
83 | + setListHeight(); | |
84 | + getDataSource(); | |
85 | + }); | |
86 | +</script> | |
87 | + | |
88 | +<template> | |
89 | + <PageWrapper class="bg-gray-100 device-task-list-container"> | |
90 | + <section | |
91 | + class="form-container bg-light-50 px-4 pt-4 mt-4 x dark:text-gray-300 dark:bg-dark-900" | |
92 | + > | |
93 | + <BasicForm @register="registerForm" /> | |
94 | + </section> | |
95 | + <section class="bg-light-50 my-4 p-4 x dark:text-gray-300 dark:bg-dark-900"> | |
96 | + <List | |
97 | + ref="listElRef" | |
98 | + :dataSource="dataSource" | |
99 | + :pagination="pagination" | |
100 | + :grid="{ gutter: 16, xs: 1, sm: 1, md: 1, lg: 2, xl: 2, xxl: 3, column: 3 }" | |
101 | + :loading="loading" | |
102 | + > | |
103 | + <template #header> | |
104 | + <section class="flex justify-between gap-4 min-h-12 items-center"> | |
105 | + <div class="text-lg font-semibold"> | |
106 | + <span>任务列表</span> | |
107 | + </div> | |
108 | + <Tooltip title="刷新"> | |
109 | + <Button type="primary" @click="getDataSource"> | |
110 | + <ReloadOutlined :spin="loading" /> | |
111 | + </Button> | |
112 | + </Tooltip> | |
113 | + </section> | |
114 | + </template> | |
115 | + <template #renderItem="{ item }"> | |
116 | + <List.Item :key="item.id"> | |
117 | + <TaskCard | |
118 | + :record="item" | |
119 | + :reload="reload" | |
120 | + :tbDeviceId="tbDeviceId" | |
121 | + :deviceTaskCardMode="true" | |
122 | + /> | |
123 | + </List.Item> | |
124 | + </template> | |
125 | + </List> | |
126 | + </section> | |
127 | + </PageWrapper> | |
128 | +</template> | |
129 | + | |
130 | +<style lang="less" scoped> | |
131 | + .device-task-list-container { | |
132 | + :deep(.ant-list-header) { | |
133 | + border: none; | |
134 | + } | |
135 | + | |
136 | + :deep(.ant-card-body) { | |
137 | + padding: 16px 24px; | |
138 | + } | |
139 | + } | |
140 | +</style> | ... | ... |
... | ... | @@ -9,6 +9,14 @@ export enum EJobGroup { |
9 | 9 | DEFAULT = 'DEFAULT', |
10 | 10 | SYSTEM = 'SYSTEM', |
11 | 11 | REPORT = 'REPORT', |
12 | + TASK_CENTER = 'TASK_CENTER', | |
13 | +} | |
14 | + | |
15 | +export enum EJobGroupName { | |
16 | + DEFAULT = '默认', | |
17 | + SYSTEM = '系统', | |
18 | + REPORT = '报表', | |
19 | + TASK_CENTER = '任务中心', | |
12 | 20 | } |
13 | 21 | |
14 | 22 | //任务详细配置 |
... | ... | @@ -16,12 +24,8 @@ export const personSchema: DescItem[] = [ |
16 | 24 | { |
17 | 25 | field: 'jobGroup', |
18 | 26 | label: '任务分组:', |
19 | - render: (_, data) => { | |
20 | - return data.jobGroup == EJobGroup.DEFAULT | |
21 | - ? '默认' | |
22 | - : data.jobGroup == EJobGroup.SYSTEM | |
23 | - ? '系统' | |
24 | - : '报表'; | |
27 | + render: (value: EJobGroup) => { | |
28 | + return EJobGroupName[value]; | |
25 | 29 | }, |
26 | 30 | }, |
27 | 31 | { |
... | ... | @@ -108,8 +112,8 @@ export const columnSchedue: BasicColumn[] = [ |
108 | 112 | const status = record.status; |
109 | 113 | const success = status === 1; |
110 | 114 | const color = success ? 'green' : 'red'; |
111 | - const successText: string = '成功'; | |
112 | - const failedText: string = '失败'; | |
115 | + const successText = '成功'; | |
116 | + const failedText = '失败'; | |
113 | 117 | const text = success ? successText : failedText; |
114 | 118 | return h(Tag, { color: color }, () => text); |
115 | 119 | }, | ... | ... |
1 | 1 | import { BasicColumn, FormSchema } from '/@/components/Table'; |
2 | 2 | import type { FormSchema as QFormSchema } from '/@/components/Form/index'; |
3 | 3 | import { JCronValidator } from '/@/components/Form'; |
4 | -import { EJobGroup } from './config.data'; | |
4 | +import { EJobGroup, EJobGroupName } from './config.data'; | |
5 | 5 | |
6 | 6 | // 定时任务表格配置 |
7 | 7 | export const columnSchedue: BasicColumn[] = [ |
... | ... | @@ -14,12 +14,8 @@ export const columnSchedue: BasicColumn[] = [ |
14 | 14 | title: '任务组名', |
15 | 15 | dataIndex: 'jobGroup', |
16 | 16 | width: 120, |
17 | - format: (_text: string, record: Recordable) => { | |
18 | - return record.jobGroup === EJobGroup.DEFAULT | |
19 | - ? '默认' | |
20 | - : record.jobGroup === EJobGroup.SYSTEM | |
21 | - ? '系统' | |
22 | - : '报表'; | |
17 | + format: (text: string) => { | |
18 | + return EJobGroupName[text]; | |
23 | 19 | }, |
24 | 20 | }, |
25 | 21 | { |
... | ... | @@ -113,6 +109,7 @@ export const formSchema: QFormSchema[] = [ |
113 | 109 | field: 'jobGroup', |
114 | 110 | component: 'Select', |
115 | 111 | label: '任务分组', |
112 | + dynamicDisabled: true, | |
116 | 113 | colProps: { |
117 | 114 | span: 24, |
118 | 115 | }, |
... | ... | @@ -120,17 +117,21 @@ export const formSchema: QFormSchema[] = [ |
120 | 117 | placeholder: '请选择任务分组', |
121 | 118 | options: [ |
122 | 119 | { |
123 | - label: '默认', | |
120 | + label: EJobGroupName.DEFAULT, | |
124 | 121 | value: EJobGroup.DEFAULT, |
125 | 122 | }, |
126 | 123 | { |
127 | - label: '系统', | |
124 | + label: EJobGroupName.SYSTEM, | |
128 | 125 | value: EJobGroup.SYSTEM, |
129 | 126 | }, |
130 | 127 | { |
131 | - label: '报表', | |
128 | + label: EJobGroupName.REPORT, | |
132 | 129 | value: EJobGroup.REPORT, |
133 | 130 | }, |
131 | + { | |
132 | + label: EJobGroupName.TASK_CENTER, | |
133 | + value: EJobGroup.TASK_CENTER, | |
134 | + }, | |
134 | 135 | ], |
135 | 136 | }, |
136 | 137 | }, |
... | ... | @@ -153,6 +154,7 @@ export const formSchema: QFormSchema[] = [ |
153 | 154 | label: 'Cron表达式', |
154 | 155 | component: 'JEasyCron', |
155 | 156 | defaultValue: '* * * * * ? *', |
157 | + dynamicDisabled: true, | |
156 | 158 | colProps: { |
157 | 159 | span: 24, |
158 | 160 | }, | ... | ... |
1 | +export { default as ControlGroup } from './index.vue'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { BasicForm, FormSchema, useForm } from '/@/components/Form'; | |
3 | + import { ComponentType, ColEx } from '/@/components/Form/src/types/index'; | |
4 | + import { computed } from '@vue/reactivity'; | |
5 | + import { isFunction } from '/@/utils/is'; | |
6 | + import { unref } from 'vue'; | |
7 | + import { watch } from 'vue'; | |
8 | + import { nextTick } from 'vue'; | |
9 | + import { ref } from 'vue'; | |
10 | + import { onMounted } from 'vue'; | |
11 | + | |
12 | + interface ValueItemType { | |
13 | + value: any; | |
14 | + } | |
15 | + | |
16 | + enum FormFieldsEnum { | |
17 | + TOTAL_CONTROL = 'totalControl', | |
18 | + } | |
19 | + | |
20 | + enum EmitEventEnum { | |
21 | + UPDATE_VALUE = 'update:value', | |
22 | + } | |
23 | + | |
24 | + const emit = defineEmits<{ | |
25 | + (event: EmitEventEnum.UPDATE_VALUE, value: ValueItemType[]): void; | |
26 | + }>(); | |
27 | + | |
28 | + const props = withDefaults( | |
29 | + defineProps<{ | |
30 | + value: ValueItemType[]; | |
31 | + length?: number; | |
32 | + component?: ComponentType; | |
33 | + itemColProps?: Partial<ColEx>; | |
34 | + itemLabel?: (index: number) => string; | |
35 | + itemProps?: (index: number) => FormSchema; | |
36 | + showTotalControl?: boolean; | |
37 | + totalControlProps?: FormSchema; | |
38 | + }>(), | |
39 | + { | |
40 | + value: () => [], | |
41 | + length: 0, | |
42 | + component: 'Switch', | |
43 | + itemLabel: (index: number) => `#${index}`, | |
44 | + itemProps: () => ({} as unknown as FormSchema), | |
45 | + itemColProps: () => ({ span: 12 } as Partial<ColEx>), | |
46 | + showTotalControl: true, | |
47 | + totalControlProps: () => ({} as unknown as FormSchema), | |
48 | + } | |
49 | + ); | |
50 | + | |
51 | + const getProps = computed(() => { | |
52 | + return props; | |
53 | + }); | |
54 | + | |
55 | + const batchSetValue = (value: any): ValueItemType[] => { | |
56 | + const { length } = unref(getProps); | |
57 | + return Array.from({ length }, () => ({ value })); | |
58 | + }; | |
59 | + | |
60 | + const getTotalControlItem = computed(() => { | |
61 | + const { totalControlProps, component, showTotalControl } = unref(getProps); | |
62 | + return { | |
63 | + ...totalControlProps, | |
64 | + field: FormFieldsEnum.TOTAL_CONTROL, | |
65 | + component, | |
66 | + ifShow: showTotalControl, | |
67 | + componentProps: { | |
68 | + onChange(value: any) { | |
69 | + handleUpdateValue(batchSetValue(value)); | |
70 | + }, | |
71 | + }, | |
72 | + } as FormSchema; | |
73 | + }); | |
74 | + | |
75 | + const getSchemas = computed(() => { | |
76 | + const { itemProps, itemLabel, length, component } = unref(getProps); | |
77 | + let label = isFunction(itemLabel) ? itemLabel : (index: number) => `#${index}`; | |
78 | + let _itemProps = isFunction(itemProps) ? itemProps : () => ({}); | |
79 | + const schemas = Array.from( | |
80 | + { length }, | |
81 | + (_item, index) => | |
82 | + ({ | |
83 | + ..._itemProps(index), | |
84 | + label: label(index), | |
85 | + field: index.toString(), | |
86 | + component, | |
87 | + componentProps: { | |
88 | + onChange: async () => { | |
89 | + await nextTick(); | |
90 | + handleUpdateValue(); | |
91 | + }, | |
92 | + }, | |
93 | + } as FormSchema) | |
94 | + ); | |
95 | + | |
96 | + length && schemas.unshift(unref(getTotalControlItem)); | |
97 | + | |
98 | + return schemas; | |
99 | + }); | |
100 | + | |
101 | + const [registerForm, { getFieldsValue, setProps, setFieldsValue }] = useForm({ | |
102 | + showActionButtonGroup: false, | |
103 | + schemas: unref(getSchemas), | |
104 | + // baseColProps, | |
105 | + baseColProps: props.itemColProps, | |
106 | + }); | |
107 | + | |
108 | + const handleUpdateValue = (value?: ValueItemType[]) => { | |
109 | + if (value) { | |
110 | + emit(EmitEventEnum.UPDATE_VALUE, value); | |
111 | + return; | |
112 | + } | |
113 | + const allValue = getFieldsValue(); | |
114 | + const sortKeyList = Array.from({ length: unref(getProps).length }, (_v, key) => key); | |
115 | + const res = sortKeyList.map((item) => ({ value: allValue[item] } as ValueItemType)); | |
116 | + | |
117 | + emit(EmitEventEnum.UPDATE_VALUE, res); | |
118 | + }; | |
119 | + | |
120 | + const transformValue = (value: ValueItemType[]) => { | |
121 | + const { length } = unref(getProps); | |
122 | + if (value.length !== length) { | |
123 | + value = Array.from( | |
124 | + { length: unref(getProps).length }, | |
125 | + () => ({ value: null } as ValueItemType) | |
126 | + ); | |
127 | + } | |
128 | + return value.reduce((prev, next, index) => ({ ...prev, [index]: next.value }), {}); | |
129 | + }; | |
130 | + | |
131 | + const initialized = ref(false); | |
132 | + | |
133 | + watch( | |
134 | + () => props.value, | |
135 | + async (target) => { | |
136 | + if (target) { | |
137 | + let flag = unref(initialized); | |
138 | + if (!flag) { | |
139 | + await nextTick(); | |
140 | + } | |
141 | + const value = transformValue(target); | |
142 | + setFieldsValue(value); | |
143 | + | |
144 | + if (!flag) { | |
145 | + handleUpdateValue(); | |
146 | + } | |
147 | + } | |
148 | + }, | |
149 | + { | |
150 | + immediate: true, | |
151 | + } | |
152 | + ); | |
153 | + | |
154 | + watch( | |
155 | + () => [props.length, props.component], | |
156 | + (target) => { | |
157 | + if (target !== undefined || target !== null) { | |
158 | + setProps({ | |
159 | + schemas: unref(getSchemas), | |
160 | + }); | |
161 | + handleUpdateValue(); | |
162 | + } | |
163 | + } | |
164 | + ); | |
165 | + | |
166 | + onMounted(() => { | |
167 | + initialized.value = true; | |
168 | + }); | |
169 | +</script> | |
170 | + | |
171 | +<template> | |
172 | + <BasicForm class="control-group-form" @register="registerForm" /> | |
173 | +</template> | |
174 | + | |
175 | +<style lang="less" scoped> | |
176 | + .control-group-form { | |
177 | + :deep(.ant-form-item-label) { | |
178 | + font-weight: 700; | |
179 | + } | |
180 | + } | |
181 | +</style> | ... | ... |
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 { PollCommandInput, ModeEnum } from '../PollCommandInput'; | |
10 | +import { DeviceProfileModel } from '/@/api/device/model/deviceModel'; | |
11 | +import { TransportTypeEnum } from '/@/views/device/profiles/components/TransportDescript/const'; | |
12 | +import { JSONEditorValidator } from '/@/components/CodeEditor/src/JSONEditor'; | |
13 | +import { TimeUnitEnum, TimeUnitNameEnum } from '/@/enums/toolEnum'; | |
14 | +import { dateUtil } from '/@/utils/dateUtil'; | |
15 | +import { ProductPicker, validateProductPicker } from '../ProductPicker'; | |
16 | + | |
17 | +useComponentRegister('DevicePicker', DevicePicker); | |
18 | +useComponentRegister('ProductPicker', ProductPicker); | |
19 | +useComponentRegister('PollCommandInput', PollCommandInput); | |
20 | + | |
21 | +export enum FormFieldsEnum { | |
22 | + // 任务名称 | |
23 | + NAME = 'name', | |
24 | + // 目标类型 | |
25 | + TARGET_TYPE = 'targetType', | |
26 | + // 设备类型选择 | |
27 | + DEVICE_PROFILE = 'deviceProfile', | |
28 | + // 执行目标源 | |
29 | + EXECUTE_TARGET_DATA = 'executeTargetData', | |
30 | + // 执行任务类型 | |
31 | + EXECUTE_CONTENT_TYPE = 'executeContentType', | |
32 | + // 下发命令 | |
33 | + RPC_COMMAND = 'rpcCommand', | |
34 | + // 推送方式 | |
35 | + PUSH_WAY = 'pushWay', | |
36 | + // 执行周期类型 | |
37 | + EXECUTE_TIME_TYPE = 'executeTimeType', | |
38 | + // 执行间隔时间 | |
39 | + EXECUTE_TIME_INTERVAL = 'interval', | |
40 | + // 执行周期类型 | |
41 | + EXECUTE_TIME_PERIOD_TYPE = 'periodType', | |
42 | + // 周期 每月 每周 | |
43 | + EXECUTE_TIME_PERIOD = 'period', | |
44 | + // time时间 | |
45 | + TIME = 'time', | |
46 | + // 设备传输协议 | |
47 | + TRANSPORT_TYPE = 'transportType', | |
48 | + // 间隔时间单位 | |
49 | + POLL_UNIT = 'pollUnit', | |
50 | +} | |
51 | + | |
52 | +export enum PushWayEnum { | |
53 | + MQTT = 'MQTT', | |
54 | + TCP = 'TCP', | |
55 | +} | |
56 | + | |
57 | +export enum ExecuteTimeTypeEnum { | |
58 | + CUSTOM = 'CUSTOM', | |
59 | + POLL = 'POLL', | |
60 | +} | |
61 | + | |
62 | +export enum ExecuteTimeTypeNameEnum { | |
63 | + CUSTOM = '自定义', | |
64 | + POLL = '间隔时间重复', | |
65 | +} | |
66 | + | |
67 | +export enum PeriodTypeEnum { | |
68 | + MONTH = 'MONTH', | |
69 | + WEEK = 'WEEK', | |
70 | + DAY = 'DAY', | |
71 | +} | |
72 | + | |
73 | +export enum PeriodTypeNameEnum { | |
74 | + MONTH = '每月', | |
75 | + WEEK = '每周', | |
76 | + DAY = '每日', | |
77 | +} | |
78 | + | |
79 | +const isShowCustomIntervalTimeSetting = (model: Recordable, flag: PeriodTypeEnum) => { | |
80 | + return ( | |
81 | + model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.CUSTOM && | |
82 | + model[FormFieldsEnum.EXECUTE_TIME_PERIOD_TYPE] === flag | |
83 | + ); | |
84 | +}; | |
85 | + | |
86 | +export const formSchemas: FormSchema[] = [ | |
87 | + { | |
88 | + field: FormFieldsEnum.NAME, | |
89 | + component: 'Input', | |
90 | + label: '任务名称', | |
91 | + rules: [{ required: true, message: '请填写任务名称' }], | |
92 | + componentProps: { | |
93 | + placeholder: '请输入任务名称', | |
94 | + }, | |
95 | + }, | |
96 | + { | |
97 | + field: FormFieldsEnum.TARGET_TYPE, | |
98 | + component: 'RadioGroup', | |
99 | + label: '目标类型', | |
100 | + defaultValue: TaskTargetEnum.DEVICES, | |
101 | + helpMessage: ['执行任务的目标设备,可以是多个指定的设备,也可以是一个设备类型下的所有设备.'], | |
102 | + componentProps: { | |
103 | + options: [ | |
104 | + { label: TaskTargetNameEnum.DEVICES, value: TaskTargetEnum.DEVICES }, | |
105 | + { label: TaskTargetNameEnum.PRODUCTS, value: TaskTargetEnum.PRODUCTS }, | |
106 | + ], | |
107 | + }, | |
108 | + }, | |
109 | + { | |
110 | + field: FormFieldsEnum.DEVICE_PROFILE, | |
111 | + component: 'ProductPicker', | |
112 | + label: '产品', | |
113 | + ifShow: ({ model }) => model[FormFieldsEnum.TARGET_TYPE] === TaskTargetEnum.PRODUCTS, | |
114 | + valueField: 'value', | |
115 | + changeEvent: 'update:value', | |
116 | + rules: [validateProductPicker()], | |
117 | + helpMessage: ['任务可以对目标产品按预设的时间策略执行任务。'], | |
118 | + componentProps: ({ formActionType }) => { | |
119 | + const { setFieldsValue } = formActionType; | |
120 | + return { | |
121 | + onChange(key: string, _value: string | string[], option: DeviceProfileModel) { | |
122 | + if (key === DeviceCascadePickerFieldsEnum.DEVICE_PROFILE) { | |
123 | + const isTCP = (option || {}).transportType === TransportTypeEnum.TCP; | |
124 | + setFieldsValue({ | |
125 | + [FormFieldsEnum.TRANSPORT_TYPE]: _value ? option.transportType : null, | |
126 | + [FormFieldsEnum.PUSH_WAY]: isTCP ? PushWayEnum.TCP : PushWayEnum.MQTT, | |
127 | + ...(isTCP ? {} : { [FormFieldsEnum.EXECUTE_CONTENT_TYPE]: TaskTypeEnum.CUSTOM }), | |
128 | + }); | |
129 | + } | |
130 | + }, | |
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.TCP, | |
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: 'PollCommandInput', | |
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: 'PollCommandInput', | |
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 | + defaultOpenValue: dateUtil('00:00:00', 'HH:mm:ss'), | |
321 | + }, | |
322 | + }, | |
323 | + { | |
324 | + field: FormFieldsEnum.POLL_UNIT, | |
325 | + component: 'RadioGroup', | |
326 | + label: '时间单位', | |
327 | + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.POLL, | |
328 | + defaultValue: TimeUnitEnum.SECOND, | |
329 | + componentProps: { | |
330 | + options: [ | |
331 | + { label: TimeUnitNameEnum.SECOND, value: TimeUnitEnum.SECOND }, | |
332 | + { label: TimeUnitNameEnum.MINUTE, value: TimeUnitEnum.MINUTE }, | |
333 | + { label: TimeUnitNameEnum.HOUR, value: TimeUnitEnum.HOUR }, | |
334 | + ], | |
335 | + }, | |
336 | + }, | |
337 | + { | |
338 | + field: FormFieldsEnum.EXECUTE_TIME_INTERVAL, | |
339 | + label: '间隔时间', | |
340 | + component: 'InputNumber', | |
341 | + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.POLL, | |
342 | + componentProps: ({ formModel }) => { | |
343 | + const unit = formModel[FormFieldsEnum.POLL_UNIT]; | |
344 | + return { | |
345 | + min: 0, | |
346 | + max: unit === TimeUnitEnum.HOUR ? 23 : 59, | |
347 | + step: 1, | |
348 | + placeholder: '请输入间隔时间', | |
349 | + }; | |
350 | + }, | |
351 | + }, | |
352 | +]; | ... | ... |
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 | + setFieldsValue({ ...res }); | |
35 | + } | |
36 | + } | |
37 | + ); | |
38 | + | |
39 | + const [registerForm, { getFieldsValue, validate, setFieldsValue, resetFields }] = useForm({ | |
40 | + schemas: formSchemas, | |
41 | + showActionButtonGroup: false, | |
42 | + layout: 'inline', | |
43 | + baseColProps: { span: 24 }, | |
44 | + labelWidth: 140, | |
45 | + }); | |
46 | + | |
47 | + const loading = ref(false); | |
48 | + const handleOk = async () => { | |
49 | + try { | |
50 | + loading.value = true; | |
51 | + await validate(); | |
52 | + const res = getFieldsValue(); | |
53 | + const _res = composeData(res as Required<FormValueType>); | |
54 | + formMode.value === DataActionModeEnum.CREATE | |
55 | + ? await createTask(_res) | |
56 | + : await updateTask({ ..._res, id: unref(dataSource)?.id as string }); | |
57 | + closeModal(); | |
58 | + props.reload?.(); | |
59 | + } catch (error) { | |
60 | + throw error; | |
61 | + } finally { | |
62 | + loading.value = false; | |
63 | + } | |
64 | + }; | |
65 | +</script> | |
66 | + | |
67 | +<template> | |
68 | + <BasicModal | |
69 | + @register="registerModal" | |
70 | + title="创建任务" | |
71 | + width="700px" | |
72 | + :okButtonProps="{ loading }" | |
73 | + @ok="handleOk" | |
74 | + > | |
75 | + <BasicForm @register="registerForm" class="form-container" /> | |
76 | + </BasicModal> | |
77 | +</template> | |
78 | + | |
79 | +<style lang="less" scoped> | |
80 | + .form-container { | |
81 | + :deep(.ant-input-number) { | |
82 | + width: 100%; | |
83 | + } | |
84 | + } | |
85 | +</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 | +import { ProductCascadePickerValueType } from '../ProductPicker'; | |
9 | + | |
10 | +export interface FormValueType extends Partial<Record<FormFieldsEnum, any>> { | |
11 | + [FormFieldsEnum.EXECUTE_TARGET_DATA]: DeviceCascadePickerValueType; | |
12 | + [FormFieldsEnum.DEVICE_PROFILE]: ProductCascadePickerValueType; | |
13 | +} | |
14 | + | |
15 | +type CanWrite<T> = { | |
16 | + -readonly [K in keyof T]: T[K]; | |
17 | +}; | |
18 | + | |
19 | +interface GenCronExpressionResultType { | |
20 | + effective: boolean; | |
21 | + expression?: string; | |
22 | +} | |
23 | + | |
24 | +export const usePluginGenCronExpression = ( | |
25 | + time: string, | |
26 | + expression = '* * * * * * *', | |
27 | + includesYear = true | |
28 | +): GenCronExpressionResultType => { | |
29 | + try { | |
30 | + const separator = ' '; | |
31 | + const removeYear = expression.split(separator).slice(0, 6).join(separator); | |
32 | + | |
33 | + const date = dateUtil(time, 'HH:mm:ss'); | |
34 | + | |
35 | + const second = date.get('second') as SixtyRange; | |
36 | + const minute = date.get('minute') as SixtyRange; | |
37 | + const hour = date.get('hour') as HourRange; | |
38 | + | |
39 | + const result = CronParser.parseExpression(removeYear, { utc: true, nthDayOfWeek: 4 }); | |
40 | + const fields = JSON.parse(JSON.stringify(result.fields)) as CanWrite<CronFields>; | |
41 | + fields.second = [second]; | |
42 | + fields.minute = [minute]; | |
43 | + fields.hour = [hour]; | |
44 | + | |
45 | + // console.log(CronParser.fieldsToExpression(CronParser.parseExpression('').fields).stringify()); | |
46 | + | |
47 | + let newExpression = CronParser.fieldsToExpression(fields).stringify(true); | |
48 | + newExpression = includesYear ? `${newExpression} *` : newExpression; | |
49 | + return { effective: true, expression: newExpression }; | |
50 | + } catch (error) { | |
51 | + // throw error; | |
52 | + return { effective: false }; | |
53 | + } | |
54 | +}; | |
55 | + | |
56 | +export const genCronExpression = ( | |
57 | + time: string, | |
58 | + expression = '* * * * * * *' | |
59 | +): GenCronExpressionResultType => { | |
60 | + try { | |
61 | + const separator = ' '; | |
62 | + const list: (string | number)[] = expression.split(separator); | |
63 | + | |
64 | + const date = dateUtil(time, 'HH:mm:ss'); | |
65 | + | |
66 | + const second = date.get('second') as SixtyRange; | |
67 | + const minute = date.get('minute') as SixtyRange; | |
68 | + const hour = date.get('hour') as HourRange; | |
69 | + | |
70 | + list[0] = second; | |
71 | + list[1] = minute; | |
72 | + list[2] = hour; | |
73 | + | |
74 | + return { effective: true, expression: list.join(separator) }; | |
75 | + } catch (error) { | |
76 | + // throw error; | |
77 | + return { effective: false }; | |
78 | + } | |
79 | +}; | |
80 | + | |
81 | +export const composeData = (result: Required<FormValueType>): CreateTaskRecordType => { | |
82 | + const { | |
83 | + name, | |
84 | + targetType, | |
85 | + rpcCommand, | |
86 | + pushWay, | |
87 | + executeContentType, | |
88 | + executeTargetData, | |
89 | + deviceProfile, | |
90 | + executeTimeType, | |
91 | + period, | |
92 | + periodType, | |
93 | + time, | |
94 | + interval, | |
95 | + pollUnit, | |
96 | + } = result; | |
97 | + | |
98 | + const { organizationId, deviceType, deviceId, deviceProfileId } = executeTargetData || {}; | |
99 | + | |
100 | + const { | |
101 | + organizationId: productOrg, | |
102 | + deviceProfileId: productId, | |
103 | + deviceType: productDeviceType, | |
104 | + } = deviceProfile || {}; | |
105 | + | |
106 | + const { expression } = genCronExpression(time, period); | |
107 | + | |
108 | + const cron = | |
109 | + executeTimeType === ExecuteTimeTypeEnum.POLL ? `0/${interval} * * * * ? *` : expression!; | |
110 | + | |
111 | + return { | |
112 | + name, | |
113 | + targetType, | |
114 | + executeContent: { | |
115 | + pushContent: { | |
116 | + rpcCommand: pushWay === PushWayEnum.MQTT ? JSON.parse(rpcCommand) : rpcCommand, | |
117 | + }, | |
118 | + pushWay, | |
119 | + type: executeContentType, | |
120 | + }, | |
121 | + executeTarget: { | |
122 | + data: targetType === TaskTargetEnum.PRODUCTS ? [productId] : (deviceId as string[]), | |
123 | + deviceType: targetType === TaskTargetEnum.PRODUCTS ? productDeviceType : deviceType, | |
124 | + organizationId: targetType === TaskTargetEnum.PRODUCTS ? productOrg : organizationId, | |
125 | + deviceProfileId: targetType === TaskTargetEnum.PRODUCTS ? productId : deviceProfileId, | |
126 | + }, | |
127 | + executeTime: { | |
128 | + type: executeTimeType, | |
129 | + periodType, | |
130 | + period, | |
131 | + time: executeTimeType === ExecuteTimeTypeEnum.POLL ? interval : time, | |
132 | + cron, | |
133 | + pollUnit, | |
134 | + }, | |
135 | + }; | |
136 | +}; | |
137 | + | |
138 | +export const parseData = (result: TaskRecordType): Required<FormValueType> => { | |
139 | + const { name, targetType, executeContent, executeTarget, executeTime } = result; | |
140 | + const { pushContent: { rpcCommand } = {}, pushWay, type: executeContentType } = executeContent; | |
141 | + const { data, deviceProfileId, deviceType, organizationId } = executeTarget as Required< | |
142 | + TaskRecordType['executeTarget'] | |
143 | + >; | |
144 | + const { type: executeTimeType, period, periodType, time, pollUnit } = executeTime; | |
145 | + return { | |
146 | + name, | |
147 | + targetType, | |
148 | + rpcCommand: pushWay === PushWayEnum.MQTT ? JSON.stringify(rpcCommand, null, 2) : rpcCommand, | |
149 | + transportType: pushWay, | |
150 | + pushWay, | |
151 | + executeContentType, | |
152 | + executeTargetData: { | |
153 | + deviceId: targetType === TaskTargetEnum.DEVICES ? data : [], | |
154 | + deviceProfileId, | |
155 | + deviceType, | |
156 | + organizationId, | |
157 | + }, | |
158 | + deviceProfile: | |
159 | + targetType === TaskTargetEnum.PRODUCTS | |
160 | + ? { | |
161 | + deviceProfileId: data[0], | |
162 | + deviceType, | |
163 | + organizationId, | |
164 | + } | |
165 | + : ({} as unknown as ProductCascadePickerValueType), | |
166 | + executeTimeType, | |
167 | + period, | |
168 | + periodType: executeTimeType === ExecuteTimeTypeEnum.CUSTOM ? periodType : null, | |
169 | + time: executeTimeType === ExecuteTimeTypeEnum.CUSTOM ? time : null, | |
170 | + interval: executeTimeType === ExecuteTimeTypeEnum.POLL ? time : null, | |
171 | + pollUnit: executeTimeType === ExecuteTimeTypeEnum.POLL ? pollUnit : TimeUnitEnum.SECOND, | |
172 | + }; | |
173 | +}; | ... | ... |
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 | +<script lang="ts" setup> | |
2 | + import { Button, Spin } 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 | + import { composeModbusModalData } from './util'; | |
9 | + import { ModbusCommandValueType } from './type'; | |
10 | + import { genModbusCommand } from '/@/api/task'; | |
11 | + | |
12 | + const emit = defineEmits(['update:value']); | |
13 | + | |
14 | + const [registerModal] = useModal(); | |
15 | + | |
16 | + const [registerForm, { getFieldsValue }] = useForm({ | |
17 | + schemas: formSchemas, | |
18 | + showActionButtonGroup: false, | |
19 | + rowProps: { gutter: 10 }, | |
20 | + baseColProps: { span: 12 }, | |
21 | + }); | |
22 | + | |
23 | + const commandValue = ref(''); | |
24 | + | |
25 | + const loading = ref(false); | |
26 | + const handleGetValue = async () => { | |
27 | + try { | |
28 | + const value = getFieldsValue(); | |
29 | + const record = composeModbusModalData(value as ModbusCommandValueType); | |
30 | + loading.value = true; | |
31 | + const result = await genModbusCommand(record); | |
32 | + console.log(result); | |
33 | + commandValue.value = result; | |
34 | + } catch (error) { | |
35 | + } finally { | |
36 | + loading.value = false; | |
37 | + } | |
38 | + }; | |
39 | + | |
40 | + const formatCommand = (command: string, subNumber: number) => { | |
41 | + const list = command.split(''); | |
42 | + let index = 0; | |
43 | + const arr: string[][] = []; | |
44 | + while (index <= list.length) { | |
45 | + arr.push(list.slice(index, (index += subNumber))); | |
46 | + } | |
47 | + return arr.reduce((prev, next) => `${prev} ${next.join('')}`, ''); | |
48 | + }; | |
49 | + | |
50 | + const handleOk = () => { | |
51 | + emit('update:value', unref(commandValue)); | |
52 | + }; | |
53 | +</script> | |
54 | + | |
55 | +<template> | |
56 | + <BasicModal | |
57 | + @register="registerModal" | |
58 | + :okButtonProps="{ loading }" | |
59 | + title="配置操作" | |
60 | + @ok="handleOk" | |
61 | + > | |
62 | + <BasicForm @register="registerForm" class="create-tcp-command-form" /> | |
63 | + <section> | |
64 | + <Button @click="handleGetValue" type="link" class="!px-0">生成预览</Button> | |
65 | + <div> | |
66 | + <div class="text-gray-400">Modbus 指令预览</div> | |
67 | + <Spin :spinning="loading" size="small"> | |
68 | + <div class="bg-dark-50 text-light-50 p-1 w-full block mt-1 min-h-8">{{ | |
69 | + formatCommand(commandValue, 2) | |
70 | + }}</div> | |
71 | + </Spin> | |
72 | + </div> | |
73 | + </section> | |
74 | + </BasicModal> | |
75 | +</template> | |
76 | + | |
77 | +<style lang="less" scoped> | |
78 | + .create-tcp-command-form { | |
79 | + :deep(.ant-input-number) { | |
80 | + width: 100%; | |
81 | + min-width: 0; | |
82 | + } | |
83 | + } | |
84 | +</style> | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { InputGroup, InputNumber, Select, Input } from 'ant-design-vue'; | |
3 | + import { unref } from 'vue'; | |
4 | + import { computed } from 'vue'; | |
5 | + import { ref } from 'vue'; | |
6 | + | |
7 | + enum AddressTypeEnum { | |
8 | + DEC = 'DEC', | |
9 | + HEX = 'HEX', | |
10 | + } | |
11 | + | |
12 | + const emit = defineEmits(['update:value']); | |
13 | + | |
14 | + const DEC_MAX_VALUE = parseInt('0xffff', 16); | |
15 | + | |
16 | + withDefaults( | |
17 | + defineProps<{ | |
18 | + value?: number | string; | |
19 | + inputProps?: Recordable; | |
20 | + }>(), | |
21 | + { | |
22 | + value: 0, | |
23 | + inputProps: () => ({}), | |
24 | + } | |
25 | + ); | |
26 | + | |
27 | + const addressTypeOptions = [ | |
28 | + { label: AddressTypeEnum.DEC, value: AddressTypeEnum.DEC }, | |
29 | + { label: AddressTypeEnum.HEX, value: AddressTypeEnum.HEX }, | |
30 | + ]; | |
31 | + | |
32 | + const type = ref(AddressTypeEnum.DEC); | |
33 | + | |
34 | + const inputValue = ref<number | string>(0); | |
35 | + | |
36 | + const getHexValue = computed(() => { | |
37 | + return parseInt(unref(inputValue) || 0, 16); | |
38 | + }); | |
39 | + | |
40 | + const getDecValue = computed(() => { | |
41 | + let formatValue = Number(unref(inputValue) || 0).toString(16); | |
42 | + formatValue = `0x${formatValue.padStart(4, '0')}`; | |
43 | + return (inputValue.value as number) > DEC_MAX_VALUE ? '0x0000' : formatValue; | |
44 | + }); | |
45 | + | |
46 | + const toDEC = (value: number | string) => { | |
47 | + return unref(type) === AddressTypeEnum.DEC | |
48 | + ? isNaN(value as number) | |
49 | + ? 0 | |
50 | + : Number(value) | |
51 | + : parseInt(value, 16); | |
52 | + }; | |
53 | + | |
54 | + const handleEmit = () => { | |
55 | + const syncValue = toDEC(unref(inputValue)); | |
56 | + emit('update:value', syncValue); | |
57 | + }; | |
58 | + | |
59 | + const handleChange = (value: AddressTypeEnum) => { | |
60 | + const syncValue = value === AddressTypeEnum.DEC ? unref(getHexValue) : unref(getDecValue); | |
61 | + inputValue.value = syncValue; | |
62 | + emit('update:value', toDEC(syncValue)); | |
63 | + }; | |
64 | +</script> | |
65 | + | |
66 | +<template> | |
67 | + <InputGroup compact class="!flex"> | |
68 | + <Select | |
69 | + v-model:value="type" | |
70 | + :options="addressTypeOptions" | |
71 | + @change="handleChange" | |
72 | + class="bg-gray-200 max-w-20" | |
73 | + /> | |
74 | + <InputNumber | |
75 | + v-if="type === AddressTypeEnum.DEC" | |
76 | + v-model:value="inputValue" | |
77 | + :step="1" | |
78 | + class="flex-1" | |
79 | + v-bind="inputProps" | |
80 | + @change="handleEmit" | |
81 | + /> | |
82 | + <Input v-if="type === AddressTypeEnum.HEX" v-model:value="inputValue" @change="handleEmit" /> | |
83 | + <div class="text-center h-8 leading-8 px-2 bg-gray-200 cursor-pointer rounded-1 w-20"> | |
84 | + <div v-if="type === AddressTypeEnum.DEC">{{ getDecValue }}</div> | |
85 | + <div v-if="type === AddressTypeEnum.HEX">{{ getHexValue }}</div> | |
86 | + </div> | |
87 | + </InputGroup> | |
88 | +</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 | + // 设备地址码 | |
10 | + DEVICE_CODE = 'deviceCode', | |
11 | + // 功能码 | |
12 | + METHOD = 'method', | |
13 | + // 寄存器地址 | |
14 | + REGISTER_ADDR = 'registerAddr', | |
15 | + // 数据校验算法 | |
16 | + CRC = 'crc', | |
17 | + // 线圈个数 | |
18 | + COIL_NUMBER = 'coilNumber', | |
19 | + // 寄存器个数 | |
20 | + REGISTER_NUMBER = 'registerNumber', | |
21 | + // 线圈值 | |
22 | + COIL_VALUE = 'coilValue', | |
23 | + // 寄存器值 | |
24 | + REGISTER_VALUE = 'registerValue', | |
25 | + // 线圈组值 | |
26 | + COIL_VALUES = 'coilValues', | |
27 | + // 寄存器组值 | |
28 | + REGISTER_VALUES = 'registerValues', | |
29 | +} | |
30 | + | |
31 | +useComponentRegister('RegisterAddressInput', RegisterAddressInput); | |
32 | +useComponentRegister('ControlGroup', ControlGroup); | |
33 | + | |
34 | +export enum FunctionCodeEnum { | |
35 | + // 读取线圈状态01 | |
36 | + READ_COIL_STATE_01 = '01', | |
37 | + // 读取输入状态02 | |
38 | + READ_INPUT_STATE_02 = '02', | |
39 | + // 读取保持寄存器 | |
40 | + READ_KEEP_REGISTER_03 = '03', | |
41 | + // 读取输入寄存器 | |
42 | + READ_INPUT_REGISTER_04 = '04', | |
43 | + // 写入耽搁线圈寄存器 | |
44 | + WRITE_SINGLE_COIL_REGISTER_05 = '05', | |
45 | + // 写入单个保持寄存器 | |
46 | + WRITE_SINGLE_KEEP_COIL_REGISTER_06 = '06', | |
47 | + // 写入多个线圈状态 | |
48 | + WRITE_MULTIPLE_COIL_STATE_15 = '15', | |
49 | + // 写入多个保持寄存器 | |
50 | + WRITE_MULTIPLE_KEEP_REGISTER_16 = '16', | |
51 | +} | |
52 | + | |
53 | +export enum CRCValidTypeEnum { | |
54 | + CRC_32_HIGH = 'CRC_32_HIGH', | |
55 | + CRC_32_LOWER = 'CRC_32_LOWER', | |
56 | + AND_TOTAL_XOR = 'AND_TOTAL_XOR', | |
57 | + TOTAL_AND_SUM = 'TOTAL_AND_SUM', | |
58 | + CRC_8 = 'CRC_8', | |
59 | + CRC_16_HIGH = 'CRC_16_HIGH', | |
60 | + CRC_16_LOWER = 'CRC_16_LOWER', | |
61 | +} | |
62 | + | |
63 | +export const showCoilNumber = (value: FunctionCodeEnum) => | |
64 | + [ | |
65 | + FunctionCodeEnum.READ_COIL_STATE_01, | |
66 | + FunctionCodeEnum.READ_INPUT_STATE_02, | |
67 | + FunctionCodeEnum.WRITE_MULTIPLE_COIL_STATE_15, | |
68 | + ].includes(value); | |
69 | + | |
70 | +export const showRegisterNumber = (value: FunctionCodeEnum) => | |
71 | + [ | |
72 | + FunctionCodeEnum.READ_KEEP_REGISTER_03, | |
73 | + FunctionCodeEnum.READ_INPUT_REGISTER_04, | |
74 | + FunctionCodeEnum.WRITE_MULTIPLE_KEEP_REGISTER_16, | |
75 | + ].includes(value); | |
76 | + | |
77 | +export const showCoilValue = (value: FunctionCodeEnum) => | |
78 | + [FunctionCodeEnum.WRITE_SINGLE_COIL_REGISTER_05].includes(value); | |
79 | + | |
80 | +export const showRegisterValue = (value: FunctionCodeEnum) => | |
81 | + [FunctionCodeEnum.WRITE_SINGLE_KEEP_COIL_REGISTER_06].includes(value); | |
82 | + | |
83 | +export const isWriteCoilGroup = (value: FunctionCodeEnum) => | |
84 | + [FunctionCodeEnum.WRITE_MULTIPLE_COIL_STATE_15].includes(value); | |
85 | + | |
86 | +export const isWriteRegisterGroup = (value: FunctionCodeEnum) => | |
87 | + [FunctionCodeEnum.WRITE_MULTIPLE_KEEP_REGISTER_16].includes(value); | |
88 | + | |
89 | +export const formSchemas: FormSchema[] = [ | |
90 | + { | |
91 | + field: FormFieldsEnum.DEVICE_CODE, | |
92 | + component: 'ApiSelect', | |
93 | + label: '从机地址', | |
94 | + rules: [{ required: true, message: '请选择从机地址' }], | |
95 | + defaultValue: '01', | |
96 | + componentProps: () => { | |
97 | + return { | |
98 | + api: async (params: Recordable) => { | |
99 | + try { | |
100 | + const result = await findDictItemByCode(params); | |
101 | + return result.map((item, index) => ({ | |
102 | + ...item, | |
103 | + itemText: `${index + 1} - ${item.itemText}`, | |
104 | + })); | |
105 | + } catch (error) { | |
106 | + return []; | |
107 | + } | |
108 | + }, | |
109 | + params: { | |
110 | + dictCode: DictEnum.SLAVE_ADDRESS, | |
111 | + }, | |
112 | + labelField: 'itemText', | |
113 | + valueField: 'itemValue', | |
114 | + ...createPickerSearch(), | |
115 | + getPopupContainer: () => document.body, | |
116 | + }; | |
117 | + }, | |
118 | + }, | |
119 | + { | |
120 | + field: FormFieldsEnum.METHOD, | |
121 | + component: 'ApiSelect', | |
122 | + label: '功能码', | |
123 | + defaultValue: '01', | |
124 | + rules: [{ required: true, message: '请选择功能码' }], | |
125 | + componentProps: () => { | |
126 | + return { | |
127 | + api: findDictItemByCode, | |
128 | + params: { | |
129 | + dictCode: DictEnum.FUNCTION_CODE, | |
130 | + }, | |
131 | + labelField: 'itemText', | |
132 | + valueField: 'itemValue', | |
133 | + getPopupContainer: () => document.body, | |
134 | + }; | |
135 | + }, | |
136 | + }, | |
137 | + { | |
138 | + field: FormFieldsEnum.REGISTER_ADDR, | |
139 | + label: '起始寄存器地址', | |
140 | + component: 'RegisterAddressInput', | |
141 | + valueField: 'value', | |
142 | + changeEvent: 'update:value', | |
143 | + defaultValue: 0, | |
144 | + }, | |
145 | + { | |
146 | + field: FormFieldsEnum.REGISTER_NUMBER, | |
147 | + label: '寄存器个数', | |
148 | + component: 'InputNumber', | |
149 | + ifShow: ({ model }) => showRegisterNumber(model[FormFieldsEnum.METHOD]), | |
150 | + defaultValue: 1, | |
151 | + rules: [{ required: true, message: '请输入寄存器个数' }], | |
152 | + componentProps: { | |
153 | + min: 1, | |
154 | + max: 64, | |
155 | + step: 1, | |
156 | + placeholder: '请输入寄存器个数', | |
157 | + }, | |
158 | + }, | |
159 | + { | |
160 | + field: FormFieldsEnum.COIL_NUMBER, | |
161 | + label: '线圈个数', | |
162 | + component: 'InputNumber', | |
163 | + ifShow: ({ model }) => showCoilNumber(model[FormFieldsEnum.METHOD]), | |
164 | + defaultValue: 1, | |
165 | + rules: [{ required: true, message: '请输入线圈个数' }], | |
166 | + componentProps: { | |
167 | + min: 1, | |
168 | + max: 64, | |
169 | + step: 1, | |
170 | + placeholder: '请输入线圈个数', | |
171 | + }, | |
172 | + }, | |
173 | + { | |
174 | + field: FormFieldsEnum.COIL_VALUE, | |
175 | + label: '线圈值', | |
176 | + component: 'RegisterAddressInput', | |
177 | + valueField: 'value', | |
178 | + changeEvent: 'update:value', | |
179 | + ifShow: ({ model }) => showCoilValue(model[FormFieldsEnum.METHOD]), | |
180 | + defaultValue: '0', | |
181 | + rules: [{ required: true, message: '请输入线圈值' }], | |
182 | + componentProps: { | |
183 | + placeholder: '请输入线圈值', | |
184 | + }, | |
185 | + }, | |
186 | + { | |
187 | + field: FormFieldsEnum.REGISTER_VALUE, | |
188 | + label: '寄存器值', | |
189 | + component: 'RegisterAddressInput', | |
190 | + valueField: 'value', | |
191 | + changeEvent: 'update:value', | |
192 | + ifShow: ({ model }) => showRegisterValue(model[FormFieldsEnum.METHOD]), | |
193 | + defaultValue: '0', | |
194 | + rules: [{ required: true, message: '请输入寄存器值' }], | |
195 | + componentProps: { | |
196 | + placeholder: '请输入寄存器值', | |
197 | + }, | |
198 | + }, | |
199 | + { | |
200 | + field: FormFieldsEnum.REGISTER_VALUES, | |
201 | + label: '', | |
202 | + component: 'ControlGroup', | |
203 | + colProps: { span: 24 }, | |
204 | + valueField: 'value', | |
205 | + changeEvent: 'update:value', | |
206 | + ifShow: ({ model }) => isWriteRegisterGroup(model[FormFieldsEnum.METHOD]), | |
207 | + componentProps: ({ formModel }) => { | |
208 | + const length = formModel[FormFieldsEnum.REGISTER_NUMBER]; | |
209 | + return { | |
210 | + length: length || 1, | |
211 | + itemColProps: { span: 12, style: { paddingRight: '10px' } }, | |
212 | + component: 'RegisterAddressInput', | |
213 | + itemLabel: (index: number) => `#${index} 寄存器值`, | |
214 | + showTotalControl: false, | |
215 | + itemProps: () => { | |
216 | + return { | |
217 | + defaultValue: '0', | |
218 | + } as FormSchema; | |
219 | + }, | |
220 | + }; | |
221 | + }, | |
222 | + }, | |
223 | + { | |
224 | + field: FormFieldsEnum.COIL_VALUES, | |
225 | + label: '', | |
226 | + component: 'ControlGroup', | |
227 | + colProps: { span: 24 }, | |
228 | + valueField: 'value', | |
229 | + changeEvent: 'update:value', | |
230 | + ifShow: ({ model }) => isWriteCoilGroup(model[FormFieldsEnum.METHOD]), | |
231 | + componentProps: ({ formModel }) => { | |
232 | + const length = formModel[FormFieldsEnum.COIL_NUMBER]; | |
233 | + return { | |
234 | + length: length || 1, | |
235 | + itemColProps: { span: 6 }, | |
236 | + itemLabel: (index: number) => `#${index} 线圈状态值`, | |
237 | + itemProps: (index: number) => { | |
238 | + return { | |
239 | + defaultValue: 0, | |
240 | + helpMessage: [`设置从起始地址偏移 ${index} 位的线圈开关量状态。`], | |
241 | + } as FormSchema; | |
242 | + }, | |
243 | + totalControlProps: { | |
244 | + label: '全开或全关', | |
245 | + helpMessage: ['将以下所有线圈的开关量状态全部置为 1 或 0,实现一键全开或全关.'], | |
246 | + colProps: { span: 24 }, | |
247 | + } as FormSchema, | |
248 | + }; | |
249 | + }, | |
250 | + }, | |
251 | + { | |
252 | + field: FormFieldsEnum.CRC, | |
253 | + label: '数据校验', | |
254 | + component: 'ApiSelect', | |
255 | + colProps: { span: 24 }, | |
256 | + rules: [{ required: true, message: '请选择数据校验方式' }], | |
257 | + defaultValue: CRCValidTypeEnum.CRC_16_LOWER, | |
258 | + componentProps: () => { | |
259 | + return { | |
260 | + api: findDictItemByCode, | |
261 | + params: { | |
262 | + dictCode: DictEnum.DATA_VALIDATE, | |
263 | + }, | |
264 | + labelField: 'itemText', | |
265 | + valueField: 'itemValue', | |
266 | + placeholder: '请选择数据校验方式', | |
267 | + getPopupContainer: () => document.body, | |
268 | + }; | |
269 | + }, | |
270 | + }, | |
271 | +]; | ... | ... |
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 GenModbusCommandModal from './GenModbusCommandModal.vue'; | |
8 | + import { useModal } from '/@/components/Modal'; | |
9 | + import { ref } from 'vue'; | |
10 | + import { unref } from 'vue'; | |
11 | + | |
12 | + const props = withDefaults( | |
13 | + defineProps<{ | |
14 | + value?: any; | |
15 | + mode?: string; | |
16 | + inputProps?: Recordable; | |
17 | + showSettingAddonAfter?: boolean; | |
18 | + openSettingOnInputFocus?: boolean; | |
19 | + }>(), | |
20 | + { | |
21 | + mode: 'application/json', | |
22 | + showSettingAddonAfter: true, | |
23 | + openSettingOnInputFocus: false, | |
24 | + } | |
25 | + ); | |
26 | + | |
27 | + const emit = defineEmits(['update:value']); | |
28 | + | |
29 | + const inputElRef = ref<Nullable<HTMLInputElement>>(null); | |
30 | + | |
31 | + const [registerCreateTCPCommandModal, { openModal }] = useModal(); | |
32 | + | |
33 | + const getJSONValue = computed(() => { | |
34 | + return props.value; | |
35 | + }); | |
36 | + | |
37 | + const handleEmit = (value: any) => { | |
38 | + emit('update:value', value); | |
39 | + openModal(false); | |
40 | + if (props.openSettingOnInputFocus) { | |
41 | + unref(inputElRef)?.blur(); | |
42 | + } | |
43 | + }; | |
44 | + | |
45 | + const handleClick = () => { | |
46 | + openModal(true); | |
47 | + }; | |
48 | + | |
49 | + const handleFocus = () => { | |
50 | + if (props.openSettingOnInputFocus) { | |
51 | + openModal(true); | |
52 | + unref(inputElRef)?.blur(); | |
53 | + } | |
54 | + }; | |
55 | + | |
56 | + const handleUpdateEditorValue = (value: string) => { | |
57 | + handleEmit(value); | |
58 | + }; | |
59 | +</script> | |
60 | + | |
61 | +<template> | |
62 | + <section> | |
63 | + <Input | |
64 | + v-if="mode === ModeEnum.NORMAL" | |
65 | + ref="inputElRef" | |
66 | + :value="getJSONValue" | |
67 | + @change="handleEmit" | |
68 | + v-bind="inputProps" | |
69 | + @focus="handleFocus" | |
70 | + > | |
71 | + <template v-if="showSettingAddonAfter" #addonAfter> | |
72 | + <SettingOutlined class="cursor-pointer" @click="handleClick" /> | |
73 | + </template> | |
74 | + </Input> | |
75 | + <JSONEditor | |
76 | + v-if="mode === ModeEnum.JSON" | |
77 | + :value="getJSONValue" | |
78 | + @update:value="handleUpdateEditorValue" | |
79 | + /> | |
80 | + <GenModbusCommandModal @register="registerCreateTCPCommandModal" @update:value="handleEmit" /> | |
81 | + </section> | |
82 | +</template> | ... | ... |
1 | +import { FunctionCodeEnum } from './config'; | |
2 | +import { ModbusCommandValueType } from './type'; | |
3 | +import { GenModbusCommandType } from '/@/api/task/model'; | |
4 | + | |
5 | +const registerInfo = (record: ModbusCommandValueType): Partial<GenModbusCommandType> => { | |
6 | + const { | |
7 | + method, | |
8 | + registerNumber, | |
9 | + registerValue, | |
10 | + registerValues, | |
11 | + coilNumber, | |
12 | + coilValue, | |
13 | + coilValues, | |
14 | + } = record; | |
15 | + switch (method) { | |
16 | + case FunctionCodeEnum.READ_COIL_STATE_01 || FunctionCodeEnum.READ_INPUT_STATE_02: | |
17 | + return { registerNum: coilNumber }; | |
18 | + case FunctionCodeEnum.READ_KEEP_REGISTER_03 || FunctionCodeEnum.READ_INPUT_REGISTER_04: | |
19 | + return { registerNum: registerNumber }; | |
20 | + case FunctionCodeEnum.WRITE_SINGLE_COIL_REGISTER_05: | |
21 | + return { registerValues: [coilValue] }; | |
22 | + case FunctionCodeEnum.WRITE_SINGLE_KEEP_COIL_REGISTER_06: | |
23 | + return { registerValues: [registerValue] }; | |
24 | + case FunctionCodeEnum.WRITE_MULTIPLE_COIL_STATE_15: | |
25 | + return { | |
26 | + registerNum: coilNumber, | |
27 | + registerValues: coilValues.map((item) => (item.value ? 1 : 0)), | |
28 | + }; | |
29 | + case FunctionCodeEnum.WRITE_MULTIPLE_KEEP_REGISTER_16: | |
30 | + return { | |
31 | + registerNum: registerNumber, | |
32 | + registerValues: registerValues.map((item) => item.value), | |
33 | + }; | |
34 | + default: | |
35 | + return {}; | |
36 | + } | |
37 | +}; | |
38 | + | |
39 | +export const composeModbusModalData = (record: ModbusCommandValueType): GenModbusCommandType => { | |
40 | + const { crc, deviceCode, method, registerAddr } = record; | |
41 | + return { | |
42 | + crc, | |
43 | + deviceCode, | |
44 | + method, | |
45 | + registerAddr, | |
46 | + ...registerInfo(record), | |
47 | + }; | |
48 | +}; | ... | ... |
1 | +export { default as ProductPicker } from './index.vue'; | |
2 | + | |
3 | +export enum FormFieldsEnum { | |
4 | + DEVICE_TYPE = 'deviceType', | |
5 | + DEVICE_PROFILE = 'deviceProfileId', | |
6 | + ORGANIZATION = 'organizationId', | |
7 | +} | |
8 | + | |
9 | +export type ProductCascadePickerValueType = Record<FormFieldsEnum, string>; | |
10 | + | |
11 | +export { validateProductPicker } from './utils'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { getOrganizationList } from '/@/api/system/system'; | |
3 | + import { BasicForm, useForm } from '/@/components/Form'; | |
4 | + import { copyTransFun } from '/@/utils/fnUtils'; | |
5 | + import { watch } from 'vue'; | |
6 | + import { nextTick } from 'vue'; | |
7 | + import { getDeviceProfile } from '/@/api/alarm/position'; | |
8 | + import { findDictItemByCode } from '/@/api/system/dict'; | |
9 | + import { DictEnum } from '/@/enums/dictEnum'; | |
10 | + import { FormFieldsEnum } from '.'; | |
11 | + import { isObject } from '/@/utils/is'; | |
12 | + import { createPickerSearch } from '/@/utils/pickerSearch'; | |
13 | + | |
14 | + const props = withDefaults( | |
15 | + defineProps<{ | |
16 | + value?: Recordable; | |
17 | + }>(), | |
18 | + { | |
19 | + value: () => ({}), | |
20 | + } | |
21 | + ); | |
22 | + | |
23 | + const emit = defineEmits(['update:value', 'change']); | |
24 | + | |
25 | + const handleChange = (key: string, value: string | string[], option: Recordable) => { | |
26 | + emit('change', key, value, option); | |
27 | + }; | |
28 | + | |
29 | + const handleEmit = async (key: string, value: string | string[], option: Recordable) => { | |
30 | + await nextTick(); | |
31 | + let _value = getFieldsValue(); | |
32 | + _value = { | |
33 | + ..._value, | |
34 | + | |
35 | + ...(key === FormFieldsEnum.DEVICE_TYPE | |
36 | + ? { | |
37 | + [FormFieldsEnum.DEVICE_PROFILE]: null, | |
38 | + } | |
39 | + : {}), | |
40 | + }; | |
41 | + handleChange(key, value, option); | |
42 | + emit('update:value', { ..._value }); | |
43 | + }; | |
44 | + | |
45 | + const [register, { setFieldsValue, getFieldsValue, resetFields }] = useForm({ | |
46 | + schemas: [ | |
47 | + { | |
48 | + field: FormFieldsEnum.DEVICE_TYPE, | |
49 | + component: 'ApiSelect', | |
50 | + label: '', | |
51 | + componentProps: () => { | |
52 | + return { | |
53 | + api: findDictItemByCode, | |
54 | + params: { | |
55 | + dictCode: DictEnum.DEVICE_TYPE, | |
56 | + }, | |
57 | + labelField: 'itemText', | |
58 | + valueField: 'itemValue', | |
59 | + placeholder: '请选择设备类型', | |
60 | + onChange(value: string, option: Recordable) { | |
61 | + handleEmit(FormFieldsEnum.DEVICE_TYPE, value, option); | |
62 | + }, | |
63 | + getPopupContainer: () => document.body, | |
64 | + ...createPickerSearch(), | |
65 | + }; | |
66 | + }, | |
67 | + }, | |
68 | + { | |
69 | + field: FormFieldsEnum.ORGANIZATION, | |
70 | + component: 'ApiTreeSelect', | |
71 | + label: '', | |
72 | + componentProps: () => { | |
73 | + return { | |
74 | + api: async () => { | |
75 | + try { | |
76 | + const data = await getOrganizationList(); | |
77 | + copyTransFun(data as any); | |
78 | + return data; | |
79 | + } catch (error) { | |
80 | + console.log(error); | |
81 | + return []; | |
82 | + } | |
83 | + }, | |
84 | + placeholder: '请选择组织', | |
85 | + labelField: 'name', | |
86 | + valueField: 'id', | |
87 | + childField: 'children', | |
88 | + onChange(value: string, option: Recordable) { | |
89 | + handleEmit(FormFieldsEnum.ORGANIZATION, value, option); | |
90 | + }, | |
91 | + getPopupContainer: () => document.body, | |
92 | + }; | |
93 | + }, | |
94 | + }, | |
95 | + { | |
96 | + field: FormFieldsEnum.DEVICE_PROFILE, | |
97 | + component: 'ApiSelect', | |
98 | + label: '', | |
99 | + componentProps: ({ formModel }) => { | |
100 | + const deviceType = Reflect.get(formModel, FormFieldsEnum.DEVICE_TYPE); | |
101 | + return { | |
102 | + api: async () => { | |
103 | + try { | |
104 | + return await getDeviceProfile(deviceType); | |
105 | + } catch (error) { | |
106 | + return []; | |
107 | + } | |
108 | + }, | |
109 | + placeholder: '请选择产品', | |
110 | + labelField: 'name', | |
111 | + valueField: 'id', | |
112 | + onChange(value: string, options: Recordable) { | |
113 | + handleEmit(FormFieldsEnum.DEVICE_PROFILE, value, options); | |
114 | + }, | |
115 | + getPopupContainer: () => document.body, | |
116 | + ...createPickerSearch(), | |
117 | + }; | |
118 | + }, | |
119 | + }, | |
120 | + ], | |
121 | + showActionButtonGroup: false, | |
122 | + layout: 'inline', | |
123 | + baseColProps: { span: 8 }, | |
124 | + }); | |
125 | + | |
126 | + const setValue = async () => { | |
127 | + await nextTick(); | |
128 | + if (!props.value || (isObject(props.value) && !Object.values(props.value).length)) { | |
129 | + resetFields(); | |
130 | + return; | |
131 | + } | |
132 | + setFieldsValue(props.value); | |
133 | + }; | |
134 | + | |
135 | + watch( | |
136 | + () => props.value, | |
137 | + () => { | |
138 | + setValue(); | |
139 | + }, | |
140 | + { | |
141 | + immediate: true, | |
142 | + } | |
143 | + ); | |
144 | +</script> | |
145 | + | |
146 | +<template> | |
147 | + <BasicForm @register="register" class="device-picker" /> | |
148 | +</template> | |
149 | + | |
150 | +<style lang="less" scoped> | |
151 | + .device-picker { | |
152 | + :deep(.ant-row) { | |
153 | + width: 100%; | |
154 | + } | |
155 | + | |
156 | + :deep(.ant-form-item-control-input-content) { | |
157 | + div > div { | |
158 | + width: 100%; | |
159 | + } | |
160 | + } | |
161 | + } | |
162 | +</style> | ... | ... |
1 | +import { FormFieldsEnum } from '.'; | |
2 | +import { Rule } from '/@/components/Form'; | |
3 | + | |
4 | +export const validateProductPicker = () => { | |
5 | + return { | |
6 | + required: true, | |
7 | + validateTrigger: 'blur', | |
8 | + validator(_rule: Recordable, value: Recordable, _callback: Fn) { | |
9 | + const product = Reflect.get(value || {}, FormFieldsEnum.DEVICE_PROFILE); | |
10 | + const org = Reflect.get(value || {}, FormFieldsEnum.ORGANIZATION); | |
11 | + if (!product) return Promise.reject('请选择产品'); | |
12 | + if (!org) return Promise.reject('请选择组织'); | |
13 | + return Promise.resolve(); | |
14 | + }, | |
15 | + } as Rule; | |
16 | +}; | ... | ... |
1 | +import { TaskTargetEnum } from '../../config'; | |
2 | +import { getMeetTheConditionsDevice } from '/@/api/dataBoard'; | |
3 | +import { getDevicesByDeviceIds } from '/@/api/device/deviceManager'; | |
4 | +import { TaskRecordType } from '/@/api/task/model'; | |
5 | +import { FormSchema } from '/@/components/Form'; | |
6 | +import { createPickerSearch } from '/@/utils/pickerSearch'; | |
7 | + | |
8 | +export interface FormValue extends Record<FormFieldsEnum, any> { | |
9 | + [FormFieldsEnum.TASK_RECORD]: string; | |
10 | +} | |
11 | + | |
12 | +export enum FormFieldsEnum { | |
13 | + EXECUTE_TARGET_TYPE = 'executeTarget', | |
14 | + TASK_TYPE = 'taskType', | |
15 | + TARGET_IDS = 'targetIds', | |
16 | + TASK_RECORD = 'taskRecord', | |
17 | +} | |
18 | + | |
19 | +export enum TargetType { | |
20 | + ALL = 'ALL', | |
21 | + ASSIGN = 'ASSIGN', | |
22 | +} | |
23 | + | |
24 | +export enum TargetNameType { | |
25 | + ALL = '所有目标设备', | |
26 | + ASSIGN = '指定目标设备', | |
27 | +} | |
28 | + | |
29 | +export const formSchemas: FormSchema[] = [ | |
30 | + { | |
31 | + field: 'taskName', | |
32 | + component: 'Input', | |
33 | + label: '任务名称', | |
34 | + slot: 'taskName', | |
35 | + }, | |
36 | + { | |
37 | + field: FormFieldsEnum.TASK_TYPE, | |
38 | + component: 'Input', | |
39 | + label: '任务类型', | |
40 | + slot: 'taskType', | |
41 | + }, | |
42 | + { | |
43 | + field: FormFieldsEnum.TASK_RECORD, | |
44 | + component: 'Input', | |
45 | + label: '任务详情', | |
46 | + show: false, | |
47 | + }, | |
48 | + { | |
49 | + field: FormFieldsEnum.EXECUTE_TARGET_TYPE, | |
50 | + component: 'RadioGroup', | |
51 | + label: '选择目标类型', | |
52 | + helpMessage: [ | |
53 | + '您可以对该任务关联的所有设备批量执行任务,也可以指定其中的一个或多个关联的设备来执行任务。', | |
54 | + ], | |
55 | + defaultValue: TargetType.ALL, | |
56 | + componentProps: { | |
57 | + options: [ | |
58 | + { label: TargetNameType.ALL, value: TargetType.ALL }, | |
59 | + { label: TargetNameType.ASSIGN, value: TargetType.ASSIGN }, | |
60 | + ], | |
61 | + }, | |
62 | + }, | |
63 | + { | |
64 | + field: FormFieldsEnum.TARGET_IDS, | |
65 | + component: 'ApiSelect', | |
66 | + label: '制定目标设备', | |
67 | + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_TARGET_TYPE] === TargetType.ASSIGN, | |
68 | + componentProps: ({ formModel }) => { | |
69 | + const record = JSON.parse(formModel[FormFieldsEnum.TASK_RECORD]) as TaskRecordType; | |
70 | + const isDevices = record.targetType === TaskTargetEnum.DEVICES; | |
71 | + const { executeTarget } = record; | |
72 | + const { organizationId, data } = executeTarget; | |
73 | + return { | |
74 | + api: async () => { | |
75 | + try { | |
76 | + if (isDevices) { | |
77 | + const result = await getDevicesByDeviceIds(data!); | |
78 | + return result.data; | |
79 | + } else { | |
80 | + return await getMeetTheConditionsDevice({ | |
81 | + organizationId, | |
82 | + deviceProfileId: data![0], | |
83 | + }); | |
84 | + } | |
85 | + } catch (error) { | |
86 | + return []; | |
87 | + } | |
88 | + }, | |
89 | + mode: 'multiple', | |
90 | + labelField: 'name', | |
91 | + valueField: 'tbDeviceId', | |
92 | + ...createPickerSearch(), | |
93 | + placeholder: '请选择设备', | |
94 | + getPopupContainer: () => document.body, | |
95 | + }; | |
96 | + }, | |
97 | + }, | |
98 | +]; | ... | ... |
1 | +export { default as RunTaskModal } from './index.vue'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ref } from 'vue'; | |
3 | + import { ImmediateExecuteTaskType, TaskRecordType } from '/@/api/task/model'; | |
4 | + import { BasicForm, useForm } from '/@/components/Form'; | |
5 | + import { BasicModal, useModalInner } from '/@/components/Modal'; | |
6 | + import { TaskTargetEnum, TaskTypeNameEnum } from '../../config'; | |
7 | + import { FormValue, TargetType, formSchemas } from './config'; | |
8 | + import { unref } from 'vue'; | |
9 | + import { immediateExecute } from '/@/api/task'; | |
10 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
11 | + const props = defineProps<{ | |
12 | + reload: Fn; | |
13 | + }>(); | |
14 | + | |
15 | + defineEmits(['register']); | |
16 | + | |
17 | + const dataSource = ref<TaskRecordType>(); | |
18 | + | |
19 | + const [registerModal, { closeModal }] = useModalInner((record: TaskRecordType) => { | |
20 | + resetFields(); | |
21 | + dataSource.value = record; | |
22 | + if (record) { | |
23 | + setFieldsValue({ taskRecord: JSON.stringify(record) } as FormValue); | |
24 | + } | |
25 | + }); | |
26 | + | |
27 | + const [registerForm, { setFieldsValue, getFieldsValue, resetFields }] = useForm({ | |
28 | + schemas: formSchemas, | |
29 | + showActionButtonGroup: false, | |
30 | + }); | |
31 | + | |
32 | + const composeData = (record: FormValue): ImmediateExecuteTaskType => { | |
33 | + const { executeTarget, targetIds } = record; | |
34 | + return { | |
35 | + executeTarget: | |
36 | + executeTarget === TargetType.ASSIGN | |
37 | + ? TaskTargetEnum.DEVICES | |
38 | + : unref(dataSource)!.targetType, | |
39 | + id: unref(dataSource)!.id, | |
40 | + targetIds, | |
41 | + cronExpression: unref(dataSource)!.executeTime.cron, | |
42 | + name: unref(dataSource)!.name, | |
43 | + }; | |
44 | + }; | |
45 | + | |
46 | + const loading = ref(false); | |
47 | + const { createMessage } = useMessage(); | |
48 | + const handleOk = async () => { | |
49 | + const record = getFieldsValue() as FormValue; | |
50 | + const value = composeData(record); | |
51 | + try { | |
52 | + loading.value = true; | |
53 | + const { data } = await immediateExecute(value); | |
54 | + data ? createMessage.success('运行成功') : createMessage.error('运行失败'); | |
55 | + if (data) { | |
56 | + closeModal(); | |
57 | + props.reload?.(); | |
58 | + } | |
59 | + } catch (error) { | |
60 | + throw error; | |
61 | + } finally { | |
62 | + loading.value = false; | |
63 | + } | |
64 | + }; | |
65 | +</script> | |
66 | + | |
67 | +<template> | |
68 | + <BasicModal | |
69 | + @register="registerModal" | |
70 | + title="运行任务" | |
71 | + :okButtonProps="{ loading }" | |
72 | + @ok="handleOk" | |
73 | + > | |
74 | + <BasicForm @register="registerForm"> | |
75 | + <template #taskName> | |
76 | + <div class="font-semibold"> | |
77 | + {{ dataSource?.name }} | |
78 | + </div> | |
79 | + </template> | |
80 | + <template #taskType> | |
81 | + <div class="font-semibold"> | |
82 | + {{ | |
83 | + dataSource ? TaskTypeNameEnum[dataSource.executeContent.type] : TaskTypeNameEnum.CUSTOM | |
84 | + }} | |
85 | + </div> | |
86 | + </template> | |
87 | + </BasicForm> | |
88 | + </BasicModal> | |
89 | +</template> | ... | ... |
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 { Authority } from '/@/components/Authority'; | |
9 | + import { TaskRecordType } from '/@/api/task/model'; | |
10 | + import { StateEnum, TaskTargetNameEnum, TaskTypeEnum } from '../../config'; | |
11 | + import { TaskTypeNameEnum, PermissionEnum } from '../../config'; | |
12 | + import AuthDropDown from '/@/components/Widget/AuthDropDown.vue'; | |
13 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
14 | + import { cancelTask, deleteTask, updateState } from '/@/api/task'; | |
15 | + import { computed } from '@vue/reactivity'; | |
16 | + import { unref } from 'vue'; | |
17 | + import { ref } from 'vue'; | |
18 | + import { dateUtil } from '/@/utils/dateUtil'; | |
19 | + import { DEFAULT_DATE_FORMAT } from '/@/views/visual/board/detail/config/util'; | |
20 | + | |
21 | + enum DropMenuEvent { | |
22 | + DELETE = 'DELETE', | |
23 | + EDIT = 'EDIT', | |
24 | + } | |
25 | + | |
26 | + const props = withDefaults( | |
27 | + defineProps<{ | |
28 | + record: TaskRecordType; | |
29 | + reload: Fn; | |
30 | + deviceTaskCardMode?: boolean; | |
31 | + tbDeviceId?: string; | |
32 | + }>(), | |
33 | + { | |
34 | + deviceTaskCardMode: false, | |
35 | + } | |
36 | + ); | |
37 | + | |
38 | + const emit = defineEmits(['edit', 'runTask']); | |
39 | + | |
40 | + const loading = ref(false); | |
41 | + | |
42 | + const { createMessage } = useMessage(); | |
43 | + | |
44 | + const getRecord = computed(() => { | |
45 | + return props.record; | |
46 | + }); | |
47 | + | |
48 | + const getCancelState = computed(() => { | |
49 | + return !!( | |
50 | + unref(getRecord).tkDeviceTaskCenter && unref(getRecord).tkDeviceTaskCenter?.allowState | |
51 | + ); | |
52 | + }); | |
53 | + | |
54 | + const handleDelete = async () => { | |
55 | + try { | |
56 | + await deleteTask([unref(getRecord).id]); | |
57 | + createMessage.success('删除成功'); | |
58 | + props.reload?.(); | |
59 | + } catch (error) {} | |
60 | + }; | |
61 | + | |
62 | + const handleSwitchState = async () => { | |
63 | + try { | |
64 | + loading.value = true; | |
65 | + await updateState( | |
66 | + unref(getRecord).id, | |
67 | + !unref(getRecord).state ? StateEnum.ENABLE : StateEnum.CLOSE | |
68 | + ); | |
69 | + createMessage.success('更新状态成功'); | |
70 | + props.reload?.(); | |
71 | + } catch (error) { | |
72 | + throw error; | |
73 | + } finally { | |
74 | + loading.value = false; | |
75 | + } | |
76 | + }; | |
77 | + | |
78 | + const handleCancelTask = async () => { | |
79 | + try { | |
80 | + if (!props.tbDeviceId) return; | |
81 | + loading.value = true; | |
82 | + await cancelTask({ | |
83 | + id: unref(getRecord).id, | |
84 | + tbDeviceId: props.tbDeviceId, | |
85 | + allow: !unref(getCancelState), | |
86 | + }); | |
87 | + createMessage.success('设置成功'); | |
88 | + props.reload?.(); | |
89 | + } catch (error) { | |
90 | + throw error; | |
91 | + } finally { | |
92 | + loading.value = false; | |
93 | + } | |
94 | + }; | |
95 | + | |
96 | + const getTwoDateDiff = (date: number, now = dateUtil()) => { | |
97 | + const unitList = [ | |
98 | + { radix: null, unitName: '年', unit: 'year' }, | |
99 | + { radix: 11, unitName: '月', unit: 'month' }, | |
100 | + { radix: 30, unitName: '日', unit: 'day' }, | |
101 | + { radix: 23, unitName: '时', unit: 'hour' }, | |
102 | + { radix: 59, unitName: '分', unit: 'minute' }, | |
103 | + { radix: 59, unitName: '秒', unit: 'second' }, | |
104 | + ]; | |
105 | + | |
106 | + for (let i = unitList.length - 1; i >= 0; i--) { | |
107 | + const { unit, radix, unitName } = unitList[i]; | |
108 | + const lastDate = dateUtil(date); | |
109 | + const diffValue = now.diff(lastDate, unit as any); | |
110 | + if (!radix || diffValue <= radix) { | |
111 | + return { value: diffValue, unit, unitName }; | |
112 | + } | |
113 | + } | |
114 | + }; | |
115 | + | |
116 | + const getLastExecuteTime = computed(() => { | |
117 | + if (!unref(getRecord).lastExecuteTime) return; | |
118 | + return getTwoDateDiff(unref(getRecord).lastExecuteTime!); | |
119 | + }); | |
120 | + | |
121 | + const handleRunTask = () => { | |
122 | + emit('runTask', unref(getRecord)); | |
123 | + }; | |
124 | +</script> | |
125 | + | |
126 | +<template> | |
127 | + <Card hoverable class="card-container !rounded"> | |
128 | + <div v-if="!deviceTaskCardMode" class="flex justify-end mb-4"> | |
129 | + <AuthDropDown | |
130 | + @click.stop | |
131 | + :trigger="['hover']" | |
132 | + :drop-menu-list="[ | |
133 | + { | |
134 | + text: '编辑', | |
135 | + event: DropMenuEvent.DELETE, | |
136 | + disabled: !!getRecord.state, | |
137 | + icon: 'ant-design:edit-outlined', | |
138 | + auth: PermissionEnum.UPDATE, | |
139 | + onClick: emit.bind(null, 'edit', getRecord), | |
140 | + }, | |
141 | + { | |
142 | + text: '删除', | |
143 | + event: DropMenuEvent.DELETE, | |
144 | + icon: 'ant-design:delete-outlined', | |
145 | + auth: PermissionEnum.DELETE, | |
146 | + popconfirm: { | |
147 | + title: '是否确认删除操作?', | |
148 | + onConfirm: handleDelete.bind(null), | |
149 | + }, | |
150 | + }, | |
151 | + ]" | |
152 | + /> | |
153 | + </div> | |
154 | + <div class="flex text-base font-medium justify-between mb-2"> | |
155 | + <Tooltip :title="getRecord.name" placement="topLeft"> | |
156 | + <div class="truncate max-w-48">{{ getRecord.name }}</div> | |
157 | + </Tooltip> | |
158 | + <span | |
159 | + v-if="deviceTaskCardMode" | |
160 | + class="text-xs w-12 leading-6 text-right" | |
161 | + :class="getRecord.state === StateEnum.ENABLE ? 'text-green-500' : 'text-red-500'" | |
162 | + > | |
163 | + {{ getRecord.state === StateEnum.ENABLE ? '已启用' : '未启用' }} | |
164 | + </span> | |
165 | + <Authority :value="PermissionEnum.START_TASK"> | |
166 | + <Switch | |
167 | + v-if="!deviceTaskCardMode" | |
168 | + :checked="getRecord.state === StateEnum.ENABLE" | |
169 | + :loading="loading" | |
170 | + size="small" | |
171 | + @click="handleSwitchState" | |
172 | + /> | |
173 | + </Authority> | |
174 | + </div> | |
175 | + <div class="flex gap-2 items-center"> | |
176 | + <div | |
177 | + class="rounded flex justify-center items-center w-6 h-6" | |
178 | + :class=" | |
179 | + getRecord.executeContent.type === TaskTypeEnum.MODBUS_RTU | |
180 | + ? ' bg-blue-400' | |
181 | + : 'bg-green-400' | |
182 | + " | |
183 | + > | |
184 | + <CloudSyncOutlined class="svg:fill-light-50" /> | |
185 | + </div> | |
186 | + <div class="text-gray-400 truncate"> | |
187 | + <span>{{ TaskTypeNameEnum[getRecord.executeContent.type] }}</span> | |
188 | + <span class="mx-1">-</span> | |
189 | + <span>{{ TaskTargetNameEnum[getRecord.targetType] }}</span> | |
190 | + </div> | |
191 | + </div> | |
192 | + <div class="mt-4 flex justify-between items-center gap-3"> | |
193 | + <Button size="small" @click="handleRunTask"> | |
194 | + <div class="text-xs px-1"> | |
195 | + <PlayCircleOutlined class="mr-1" /> | |
196 | + <span>运行任务</span> | |
197 | + </div> | |
198 | + </Button> | |
199 | + <Tooltip | |
200 | + v-if="getLastExecuteTime" | |
201 | + overlay-class-name="task-last-execute-time-tooltip" | |
202 | + placement="topLeft" | |
203 | + :title="`最后运行时间: ${dateUtil(getRecord.lastExecuteTime).format(DEFAULT_DATE_FORMAT)}`" | |
204 | + > | |
205 | + <div class="text-gray-400 text-xs truncate"> | |
206 | + <span class="mr-2">间隔时间重复</span> | |
207 | + <span>{{ | |
208 | + getLastExecuteTime.value | |
209 | + ? `${getLastExecuteTime.value}${getLastExecuteTime.unitName}前` | |
210 | + : '刚刚' | |
211 | + }}</span> | |
212 | + </div> | |
213 | + </Tooltip> | |
214 | + </div> | |
215 | + | |
216 | + <Authority :value="PermissionEnum.ALLOW"> | |
217 | + <section | |
218 | + v-if="deviceTaskCardMode" | |
219 | + class="border-t mt-4 pt-2 text-sm border-gray-100 flex justify-between text-gray-400" | |
220 | + > | |
221 | + <div> | |
222 | + <span>允许该设备</span> | |
223 | + <Tooltip title="设置是否允许当前设备定时执行任务。该选项不影响手动执行任务。"> | |
224 | + <QuestionCircleOutlined class="ml-1" /> | |
225 | + </Tooltip> | |
226 | + </div> | |
227 | + <div> | |
228 | + <Switch | |
229 | + size="small" | |
230 | + :loading="loading" | |
231 | + :checked="getCancelState" | |
232 | + @click="handleCancelTask" | |
233 | + /> | |
234 | + </div> | |
235 | + </section> | |
236 | + </Authority> | |
237 | + </Card> | |
238 | +</template> | |
239 | + | |
240 | +<style> | |
241 | + .task-last-execute-time-tooltip { | |
242 | + max-width: 300px; | |
243 | + } | |
244 | +</style> | ... | ... |
src/views/task/center/config/index.ts
0 → 100644
1 | +import { FormSchema } from '/@/components/Form'; | |
2 | + | |
3 | +export enum PermissionEnum { | |
4 | + CREATE = 'api:yt:task_center:add:post', | |
5 | + UPDATE = 'api:yt:task_center:update:update', | |
6 | + START_TASK = 'api:yt:task_center:update:state', | |
7 | + DELETE = 'api:yt:task_center:delete', | |
8 | + ALLOW = 'api:yt:task_center:cancel:allow', | |
9 | +} | |
10 | + | |
11 | +export enum FormFieldsEnum { | |
12 | + DEVICE_TYPE = 'targetType', | |
13 | + TASK_TYPE = 'taskType', | |
14 | + TASK_STATUS = 'state', | |
15 | +} | |
16 | + | |
17 | +export enum TaskTargetEnum { | |
18 | + DEVICES = 'DEVICES', | |
19 | + PRODUCTS = 'PRODUCTS', | |
20 | + ALL = 'all', | |
21 | +} | |
22 | + | |
23 | +export enum TaskTargetNameEnum { | |
24 | + DEVICES = '设备', | |
25 | + PRODUCTS = '产品', | |
26 | + ALL = '不限任务目标类型', | |
27 | +} | |
28 | + | |
29 | +export enum TaskTypeEnum { | |
30 | + CUSTOM = 'CUSTOM', | |
31 | + MODBUS_RTU = 'MODBUS_RTU', | |
32 | +} | |
33 | + | |
34 | +export enum TaskTypeNameEnum { | |
35 | + MODBUS_RTU = 'MODBUS_RTU轮询', | |
36 | + CUSTOM = '自定义数据下发', | |
37 | +} | |
38 | + | |
39 | +export enum TaskStatusEnum { | |
40 | + DEACTIVATE, | |
41 | + NORMAL, | |
42 | +} | |
43 | + | |
44 | +export enum TaskStatusNameEnum { | |
45 | + DEACTIVATE = '已停用', | |
46 | + NORMAL = '正常', | |
47 | +} | |
48 | + | |
49 | +export const formSchemas: FormSchema[] = [ | |
50 | + { | |
51 | + label: '目标类型', | |
52 | + component: 'Select', | |
53 | + field: FormFieldsEnum.DEVICE_TYPE, | |
54 | + componentProps: { | |
55 | + options: [ | |
56 | + { label: TaskTargetNameEnum.DEVICES, value: TaskTargetEnum.DEVICES }, | |
57 | + { label: TaskTargetNameEnum.PRODUCTS, value: TaskTargetEnum.PRODUCTS }, | |
58 | + ], | |
59 | + placeholder: '请选择目标类型', | |
60 | + getPopupContainer: () => document.body, | |
61 | + }, | |
62 | + }, | |
63 | + { | |
64 | + label: '状态', | |
65 | + component: 'Select', | |
66 | + field: FormFieldsEnum.TASK_STATUS, | |
67 | + componentProps: { | |
68 | + options: [ | |
69 | + { label: TaskStatusNameEnum.NORMAL, value: TaskStatusEnum.NORMAL }, | |
70 | + { label: TaskStatusNameEnum.DEACTIVATE, value: TaskStatusEnum.DEACTIVATE }, | |
71 | + ], | |
72 | + placeholder: '请选择状态', | |
73 | + getPopupContainer: () => document.body, | |
74 | + }, | |
75 | + }, | |
76 | +]; | |
77 | + | |
78 | +export enum StateEnum { | |
79 | + CLOSE, | |
80 | + ENABLE, | |
81 | +} | ... | ... |
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 { PermissionEnum, formSchemas } from './config'; | |
6 | + import { TaskCard } from './components/TaskCard'; | |
7 | + import { getTaskCenterList } from '/@/api/task'; | |
8 | + import { ref, unref } 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 | + import { Authority } from '/@/components/Authority'; | |
18 | + import { getBoundingClientRect } from '/@/utils/domUtils'; | |
19 | + import { RunTaskModal } from './components/RunTaskModal'; | |
20 | + | |
21 | + const [registerModal, { openModal }] = useModal(); | |
22 | + const [registerRunTaskModal, { openModal: openRunTaskModal }] = useModal(); | |
23 | + | |
24 | + const [registerForm, { getFieldsValue }] = useForm({ | |
25 | + schemas: formSchemas, | |
26 | + baseColProps: { span: 8 }, | |
27 | + compact: true, | |
28 | + showAdvancedButton: true, | |
29 | + labelWidth: 100, | |
30 | + submitFunc: async () => { | |
31 | + pagination.params = getFieldsValue(); | |
32 | + getDataSource(); | |
33 | + }, | |
34 | + }); | |
35 | + | |
36 | + const pagination = reactive({ | |
37 | + total: 10, | |
38 | + current: 1, | |
39 | + showQuickJumper: true, | |
40 | + size: 'small', | |
41 | + showTotal: (total: number) => `共 ${total} 条数据`, | |
42 | + params: {} as Recordable, | |
43 | + }); | |
44 | + | |
45 | + const dataSource = ref<TaskRecordType[]>([]); | |
46 | + const loading = ref(false); | |
47 | + const getDataSource = async () => { | |
48 | + try { | |
49 | + loading.value = true; | |
50 | + const { items } = await getTaskCenterList({ page: 1, pageSize: 10, ...pagination.params }); | |
51 | + dataSource.value = items; | |
52 | + } catch (error) { | |
53 | + throw error; | |
54 | + } finally { | |
55 | + loading.value = false; | |
56 | + } | |
57 | + }; | |
58 | + | |
59 | + const handleEdit = (record: TaskRecordType) => { | |
60 | + openModal(true, { | |
61 | + record, | |
62 | + mode: DataActionModeEnum.UPDATE, | |
63 | + } as ModalParamsType); | |
64 | + }; | |
65 | + | |
66 | + const handleRunTask = (record: TaskRecordType) => { | |
67 | + openRunTaskModal(true, record); | |
68 | + }; | |
69 | + | |
70 | + const reload = () => getDataSource(); | |
71 | + | |
72 | + const listElRef = ref<Nullable<ComponentElRef>>(null); | |
73 | + | |
74 | + const setListHeight = () => { | |
75 | + const clientHeight = document.documentElement.clientHeight; | |
76 | + const rect = getBoundingClientRect(unref(listElRef)!.$el!) as DOMRect; | |
77 | + // margin-top 24 height 24 | |
78 | + const paginationHeight = 24 + 24 + 8; | |
79 | + // list pading top 8 maring-top 8 extra slot 56 | |
80 | + const listContainerMarginBottom = 8 + 8 + 72; | |
81 | + const listContainerHeight = | |
82 | + clientHeight - rect.top - paginationHeight - listContainerMarginBottom; | |
83 | + const listContainerEl = (unref(listElRef)!.$el as HTMLElement).querySelector( | |
84 | + '.ant-spin-container' | |
85 | + ) as HTMLElement; | |
86 | + listContainerEl && | |
87 | + (listContainerEl.style.height = listContainerHeight + 'px') && | |
88 | + (listContainerEl.style.overflowY = 'auto') && | |
89 | + (listContainerEl.style.overflowX = 'hidden'); | |
90 | + }; | |
91 | + | |
92 | + onMounted(() => { | |
93 | + setListHeight(); | |
94 | + getDataSource(); | |
95 | + }); | |
96 | +</script> | |
97 | + | |
98 | +<template> | |
99 | + <PageWrapper class="task-center-container"> | |
100 | + <section | |
101 | + class="bg-light-50 flex p-4 justify-between items-center x dark:text-gray-300 dark:bg-dark-900" | |
102 | + > | |
103 | + <div class="text-2xl">任务中心</div> | |
104 | + <Authority :value="PermissionEnum.CREATE"> | |
105 | + <Button | |
106 | + type="primary" | |
107 | + @click="openModal(true, { mode: DataActionModeEnum.CREATE } as ModalParamsType)" | |
108 | + > | |
109 | + 创建任务 | |
110 | + </Button> | |
111 | + </Authority> | |
112 | + </section> | |
113 | + <section | |
114 | + class="form-container bg-light-50 px-4 pt-4 mt-4 x dark:text-gray-300 dark:bg-dark-900" | |
115 | + > | |
116 | + <BasicForm @register="registerForm" /> | |
117 | + </section> | |
118 | + <section class="bg-light-50 mt-4 p-4 dark:text-gray-300 dark:bg-dark-900"> | |
119 | + <List | |
120 | + ref="listElRef" | |
121 | + :dataSource="dataSource" | |
122 | + :pagination="pagination" | |
123 | + :grid="{ gutter: 16, xs: 1, sm: 1, md: 2, lg: 3, xl: 3, xxl: 4, column: 4 }" | |
124 | + :loading="loading" | |
125 | + > | |
126 | + <template #header> | |
127 | + <section class="flex justify-between gap-4 min-h-12 items-center"> | |
128 | + <div> | |
129 | + <span class="text-lg font-medium">任务列表</span> | |
130 | + </div> | |
131 | + <Tooltip v-if="dataSource.length" title="刷新"> | |
132 | + <Button type="primary" @click="getDataSource"> | |
133 | + <ReloadOutlined :spin="loading" /> | |
134 | + </Button> | |
135 | + </Tooltip> | |
136 | + </section> | |
137 | + </template> | |
138 | + <template #renderItem="{ item }"> | |
139 | + <List.Item :key="item.id"> | |
140 | + <TaskCard :record="item" :reload="reload" @runTask="handleRunTask" @edit="handleEdit" /> | |
141 | + </List.Item> | |
142 | + </template> | |
143 | + </List> | |
144 | + </section> | |
145 | + <DetailModal @register="registerModal" :reload="reload" /> | |
146 | + <RunTaskModal :reload="reload" @register="registerRunTaskModal" /> | |
147 | + </PageWrapper> | |
148 | +</template> | |
149 | + | |
150 | +<style lang="less" scoped> | |
151 | + .task-center-container { | |
152 | + :deep(.ant-list-header) { | |
153 | + border: none; | |
154 | + } | |
155 | + | |
156 | + :deep(.ant-card-body) { | |
157 | + padding: 16px 24px; | |
158 | + } | |
159 | + } | |
160 | +</style> | ... | ... |
1 | 1 | <script lang="ts"> |
2 | 2 | export default { |
3 | + components: { Spin }, | |
3 | 4 | inheritAttrs: false, |
4 | 5 | }; |
5 | 6 | </script> |
6 | 7 | <script lang="ts" setup> |
8 | + import { Spin } from 'ant-design-vue'; | |
7 | 9 | import { RadioRecord } from '../../detail/config/util'; |
8 | 10 | import { ControlComponentDefaultConfig, ControlComponentValue } from './control.config'; |
9 | 11 | import { useSendCommand } from './useSendCommand'; |
12 | + import { ref } from 'vue'; | |
10 | 13 | |
11 | 14 | interface VisualComponentProps<Layout = Recordable, Value = ControlComponentValue> { |
12 | 15 | value?: Value; |
... | ... | @@ -23,33 +26,45 @@ |
23 | 26 | const emit = defineEmits(['update:value', 'change']); |
24 | 27 | |
25 | 28 | const { sendCommand } = useSendCommand(); |
26 | - const handleChange = (event: Event) => { | |
29 | + | |
30 | + const loading = ref(false); | |
31 | + const handleChange = async (event: Event) => { | |
27 | 32 | const _value = (event.target as HTMLInputElement).checked; |
33 | + if (props.value) { | |
34 | + loading.value = true; | |
35 | + const flag = await sendCommand(props.value, _value); | |
36 | + loading.value = false; | |
37 | + if (!flag) { | |
38 | + (event.target as HTMLInputElement).checked = !_value; | |
39 | + return; | |
40 | + } | |
41 | + } | |
28 | 42 | emit('update:value', _value); |
29 | 43 | emit('change', _value); |
30 | - sendCommand(props.value!, _value); | |
31 | 44 | }; |
32 | 45 | </script> |
33 | 46 | |
34 | 47 | <template> |
35 | 48 | <div class="flex flex-col justify-center"> |
36 | - <label class="sliding-switch"> | |
37 | - <input | |
38 | - :value="!!Number(props.value?.value)" | |
39 | - type="checkbox" | |
40 | - :checked="!!Number(props.value?.value)" | |
41 | - @change="handleChange" | |
42 | - /> | |
43 | - <span class="slider"></span> | |
44 | - <span class="on">ON</span> | |
45 | - <span class="off">OFF</span> | |
46 | - </label> | |
47 | - <div | |
48 | - class="text-center mt-2 text-gray-700" | |
49 | - :style="{ color: props?.value?.fontColor || ControlComponentDefaultConfig.fontColor }" | |
50 | - > | |
51 | - {{ props.value?.attributeRename || props.value?.attribute }}</div | |
52 | - > | |
49 | + <Spin :spinning="loading"> | |
50 | + <label class="sliding-switch"> | |
51 | + <input | |
52 | + :value="!!Number(props.value?.value)" | |
53 | + type="checkbox" | |
54 | + :checked="!!Number(props.value?.value)" | |
55 | + @change="handleChange" | |
56 | + /> | |
57 | + <span class="slider"></span> | |
58 | + <span class="on">ON</span> | |
59 | + <span class="off">OFF</span> | |
60 | + </label> | |
61 | + <div | |
62 | + class="text-center mt-2 text-gray-700" | |
63 | + :style="{ color: props?.value?.fontColor || ControlComponentDefaultConfig.fontColor }" | |
64 | + > | |
65 | + {{ props.value?.attributeRename || props.value?.attribute }}</div | |
66 | + > | |
67 | + </Spin> | |
53 | 68 | </div> |
54 | 69 | </template> |
55 | 70 | ... | ... |
... | ... | @@ -31,8 +31,14 @@ |
31 | 31 | const checked = ref(!!Number(props.value.value)); |
32 | 32 | |
33 | 33 | const { sendCommand } = useSendCommand(); |
34 | - const handleChange = (value: boolean) => { | |
35 | - sendCommand(props.value, value); | |
34 | + const loading = ref(false); | |
35 | + const handleChange = async (value: boolean) => { | |
36 | + loading.value = true; | |
37 | + const flag = await sendCommand(props.value, value); | |
38 | + loading.value = false; | |
39 | + if (!flag) { | |
40 | + checked.value = !value; | |
41 | + } | |
36 | 42 | }; |
37 | 43 | |
38 | 44 | watchEffect(() => { |
... | ... | @@ -59,6 +65,6 @@ |
59 | 65 | {{ props.value.attributeRename || props.value.attribute }} |
60 | 66 | </span> |
61 | 67 | </div> |
62 | - <Switch v-model:checked="checked" @change="handleChange" /> | |
68 | + <Switch v-model:checked="checked" :loading="loading" @change="handleChange" /> | |
63 | 69 | </div> |
64 | 70 | </template> | ... | ... |
1 | 1 | <script lang="ts"> |
2 | 2 | export default { |
3 | + components: { Spin }, | |
3 | 4 | inheritAttrs: false, |
4 | 5 | }; |
5 | 6 | </script> |
... | ... | @@ -8,6 +9,8 @@ |
8 | 9 | import { DEFAULT_RADIO_RECORD, fontSize, RadioRecord } from '../../detail/config/util'; |
9 | 10 | import { ControlComponentDefaultConfig, ControlComponentValue } from './control.config'; |
10 | 11 | import { useSendCommand } from './useSendCommand'; |
12 | + import { ref } from 'vue'; | |
13 | + import { Spin } from 'ant-design-vue'; | |
11 | 14 | |
12 | 15 | const props = defineProps<{ |
13 | 16 | value?: ControlComponentValue; |
... | ... | @@ -18,11 +21,20 @@ |
18 | 21 | const emit = defineEmits(['update:value', 'change']); |
19 | 22 | |
20 | 23 | const { sendCommand } = useSendCommand(); |
21 | - const handleChange = (event: Event) => { | |
24 | + const loading = ref(false); | |
25 | + const handleChange = async (event: Event) => { | |
22 | 26 | const _value = (event.target as HTMLInputElement).checked; |
27 | + if (props.value) { | |
28 | + loading.value = true; | |
29 | + const flag = await sendCommand(props.value, _value); | |
30 | + loading.value = false; | |
31 | + if (!flag) { | |
32 | + (event.target as HTMLInputElement).checked = !_value; | |
33 | + return; | |
34 | + } | |
35 | + } | |
23 | 36 | emit('update:value', _value); |
24 | 37 | emit('change', _value); |
25 | - sendCommand(props.value!, _value); | |
26 | 38 | }; |
27 | 39 | |
28 | 40 | const getRadio = computed(() => { |
... | ... | @@ -32,35 +44,37 @@ |
32 | 44 | |
33 | 45 | <template> |
34 | 46 | <div class="flex flex-col"> |
35 | - <div | |
36 | - class="toggle-switch" | |
37 | - :style="{ | |
38 | - width: fontSize({ radioRecord: getRadio, basic: 75, max: 75, min: 60 }), | |
39 | - height: fontSize({ radioRecord: getRadio, basic: 97.5, max: 97.5, min: 80 }), | |
40 | - }" | |
41 | - > | |
42 | - <label class="switch"> | |
43 | - <input | |
44 | - :value="!!Number(props.value?.value)" | |
45 | - type="checkbox" | |
46 | - :checked="!!Number(props.value?.value)" | |
47 | - @change="handleChange" | |
48 | - /> | |
49 | - <div class="button"> | |
50 | - <div class="light"></div> | |
51 | - <div class="dots"></div> | |
52 | - <div class="characters"></div> | |
53 | - <div class="shine"></div> | |
54 | - <div class="shadow"></div> | |
55 | - </div> | |
56 | - </label> | |
57 | - </div> | |
58 | - <div | |
59 | - class="text-center mt-2 text-gray-700" | |
60 | - :style="{ color: props?.value?.fontColor || ControlComponentDefaultConfig.fontColor }" | |
61 | - > | |
62 | - {{ props.value?.attributeRename || props.value?.attribute }}</div | |
63 | - > | |
47 | + <Spin :spinning="loading"> | |
48 | + <div | |
49 | + class="toggle-switch" | |
50 | + :style="{ | |
51 | + width: fontSize({ radioRecord: getRadio, basic: 75, max: 75, min: 60 }), | |
52 | + height: fontSize({ radioRecord: getRadio, basic: 97.5, max: 97.5, min: 80 }), | |
53 | + }" | |
54 | + > | |
55 | + <label class="switch"> | |
56 | + <input | |
57 | + :value="!!Number(props.value?.value)" | |
58 | + type="checkbox" | |
59 | + :checked="!!Number(props.value?.value)" | |
60 | + @change="handleChange" | |
61 | + /> | |
62 | + <div class="button"> | |
63 | + <div class="light"></div> | |
64 | + <div class="dots"></div> | |
65 | + <div class="characters"></div> | |
66 | + <div class="shine"></div> | |
67 | + <div class="shadow"></div> | |
68 | + </div> | |
69 | + </label> | |
70 | + </div> | |
71 | + <div | |
72 | + class="text-center mt-2 text-gray-700" | |
73 | + :style="{ color: props?.value?.fontColor || ControlComponentDefaultConfig.fontColor }" | |
74 | + > | |
75 | + {{ props.value?.attributeRename || props.value?.attribute }}</div | |
76 | + > | |
77 | + </Spin> | |
64 | 78 | </div> |
65 | 79 | </template> |
66 | 80 | ... | ... |
... | ... | @@ -5,33 +5,44 @@ import { DeviceTypeEnum } from '/@/api/device/model/deviceModel'; |
5 | 5 | import { getModelServices } from '/@/api/device/modelOfMatter'; |
6 | 6 | import { useMessage } from '/@/hooks/web/useMessage'; |
7 | 7 | import { isString } from '/@/utils/is'; |
8 | +import { TransportTypeEnum } from '/@/views/device/profiles/components/TransportDescript/const'; | |
8 | 9 | |
9 | 10 | const { createMessage } = useMessage(); |
10 | 11 | export function useSendCommand() { |
12 | + const error = () => { | |
13 | + createMessage.error('下发指令失败'); | |
14 | + return false; | |
15 | + }; | |
11 | 16 | const sendCommand = async (record: ControlComponentValue, value: any) => { |
12 | - if (!record) return; | |
17 | + if (!record) return error(); | |
13 | 18 | const { deviceProfileId, attribute, deviceType } = record; |
14 | 19 | let { deviceId } = record; |
15 | - if (!deviceId) return; | |
20 | + if (!deviceId) return error(); | |
16 | 21 | try { |
17 | 22 | const list = await getDeviceProfile(); |
18 | 23 | const deviceProfile = list.find((item) => item.id === deviceProfileId); |
19 | - if (!deviceProfile) return; | |
24 | + if (!deviceProfile) return error(); | |
25 | + | |
20 | 26 | let params: string | Recordable = { |
21 | 27 | [attribute!]: Number(value), |
22 | 28 | }; |
23 | - if (deviceProfile.transportType === 'TCP') { | |
24 | - const serviceList = await getModelServices({ deviceProfileId: deviceProfileId! }); | |
29 | + | |
30 | + // 如果是TCP设备从物模型中获取下发命令(TCP网关子设备无物模型服务与事件) | |
31 | + if (deviceProfile!.transportType === TransportTypeEnum.TCP) { | |
32 | + const serviceList = (await getModelServices({ deviceProfileId: deviceProfileId! })) || []; | |
25 | 33 | const record = serviceList.find((item) => item.identifier === attribute); |
26 | 34 | const sendCommand = record?.functionJson.inputData?.at(0)?.serviceCommand || ''; |
27 | 35 | params = isString(sendCommand) ? sendCommand : JSON.stringify(sendCommand); |
28 | 36 | } |
37 | + | |
29 | 38 | if (deviceType === DeviceTypeEnum.SENSOR) { |
30 | 39 | deviceId = await getDeviceRelation({ |
31 | 40 | deviceId, |
32 | 41 | isSlave: deviceType === DeviceTypeEnum.SENSOR, |
33 | 42 | }); |
34 | 43 | } |
44 | + | |
45 | + // 控制按钮下发命令为0 或 1 | |
35 | 46 | await sendCommandOneway({ |
36 | 47 | deviceId, |
37 | 48 | value: { |
... | ... | @@ -44,7 +55,11 @@ export function useSendCommand() { |
44 | 55 | }, |
45 | 56 | }); |
46 | 57 | createMessage.success('命令下发成功'); |
47 | - } catch (error) {} | |
58 | + } catch (msg) { | |
59 | + return error(); | |
60 | + } finally { | |
61 | + return true; | |
62 | + } | |
48 | 63 | }; |
49 | 64 | return { |
50 | 65 | sendCommand, | ... | ... |
... | ... | @@ -57,6 +57,13 @@ |
57 | 57 | } |
58 | 58 | }; |
59 | 59 | |
60 | + const resetFormFields = async () => { | |
61 | + const hasExistEl = Object.keys(dataSourceEl).filter((key) => dataSourceEl[key]); | |
62 | + for (const id of hasExistEl) { | |
63 | + await dataSourceEl[id]?.resetFields(); | |
64 | + } | |
65 | + }; | |
66 | + | |
60 | 67 | const validate = async () => { |
61 | 68 | await basicMethod.validate(); |
62 | 69 | await validateDataSourceField(); |
... | ... | @@ -258,6 +265,16 @@ |
258 | 265 | return isControlComponent(props.frontId as FrontComponent); |
259 | 266 | }); |
260 | 267 | |
268 | + watch( | |
269 | + () => props.frontId, | |
270 | + async (target, oldTarget) => { | |
271 | + if (isControlComponent(oldTarget!)) return; | |
272 | + if (isControlComponent(target!)) { | |
273 | + await resetFormFields(); | |
274 | + } | |
275 | + } | |
276 | + ); | |
277 | + | |
261 | 278 | onMounted(() => handleSort()); |
262 | 279 | |
263 | 280 | defineExpose({ |
... | ... | @@ -293,7 +310,7 @@ |
293 | 310 | |
294 | 311 | <section ref="formListEl"> |
295 | 312 | <div v-for="item in dataSource" :data-id="item.id" :key="item.id" class="flex bg-light-50"> |
296 | - <div class="w-24 text-right flex justify-end"> 选择设备 </div> | |
313 | + <div class="w-24 text-right flex justify-end" style="flex: 0 0 96px"> 选择设备 </div> | |
297 | 314 | <div class="pl-2 flex-auto"> |
298 | 315 | <component |
299 | 316 | :frontId="$props.frontId" | ... | ... |
... | ... | @@ -9,6 +9,8 @@ import { getModelServices } from '/@/api/device/modelOfMatter'; |
9 | 9 | import { findDictItemByCode } from '/@/api/system/dict'; |
10 | 10 | import { DeviceTypeEnum } from '/@/api/device/model/deviceModel'; |
11 | 11 | import { DataTypeEnum } from '/@/components/Form/src/externalCompns/components/StructForm/config'; |
12 | +import { TransportTypeEnum } from '/@/views/device/profiles/components/TransportDescript/const'; | |
13 | +import { nextTick } from 'vue'; | |
12 | 14 | |
13 | 15 | export enum BasicConfigField { |
14 | 16 | NAME = 'name', |
... | ... | @@ -66,7 +68,7 @@ export const isMapComponent = (frontId: FrontComponent) => { |
66 | 68 | }; |
67 | 69 | |
68 | 70 | const isTcpProfile = (transportType: string) => { |
69 | - return transportType === 'TCP'; | |
71 | + return transportType === TransportTypeEnum.TCP; | |
70 | 72 | }; |
71 | 73 | |
72 | 74 | export const basicSchema: FormSchema[] = [ |
... | ... | @@ -117,6 +119,7 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For |
117 | 119 | component: 'ApiSelect', |
118 | 120 | label: '设备类型', |
119 | 121 | colProps: { span: 8 }, |
122 | + rules: [{ message: '请选择设备类型', required: true }], | |
120 | 123 | // defaultValue: DeviceTypeEnum.SENSOR, |
121 | 124 | componentProps: ({ formActionType }) => { |
122 | 125 | const { setFieldsValue } = formActionType; |
... | ... | @@ -180,7 +183,7 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For |
180 | 183 | colProps: { span: 8 }, |
181 | 184 | rules: [{ required: true, message: '组织为必填项' }], |
182 | 185 | componentProps({ formActionType }) { |
183 | - const { setFieldsValue } = formActionType; | |
186 | + const { setFieldsValue, getFieldsValue } = formActionType; | |
184 | 187 | return { |
185 | 188 | placeholder: '请选择组织', |
186 | 189 | api: async () => { |
... | ... | @@ -192,6 +195,9 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For |
192 | 195 | setFieldsValue({ |
193 | 196 | [DataSourceField.DEVICE_ID]: null, |
194 | 197 | }); |
198 | + nextTick(() => { | |
199 | + console.log('org change', getFieldsValue()); | |
200 | + }); | |
195 | 201 | }, |
196 | 202 | getPopupContainer: () => document.body, |
197 | 203 | }; |
... | ... | @@ -250,11 +256,24 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For |
250 | 256 | component: 'ApiSelect', |
251 | 257 | label: '属性', |
252 | 258 | colProps: { span: 8 }, |
253 | - rules: [{ required: true, message: '属性为必填项' }], | |
259 | + dynamicRules: ({ model }) => { | |
260 | + const transportType = model[DataSourceField.TRANSPORT_TYPE]; | |
261 | + return [ | |
262 | + { | |
263 | + required: true, | |
264 | + message: `${ | |
265 | + isControlComponent(frontId as FrontComponent) && isTcpProfile(transportType) | |
266 | + ? '服务' | |
267 | + : '属性' | |
268 | + }为必填项`, | |
269 | + }, | |
270 | + ]; | |
271 | + }, | |
254 | 272 | componentProps({ formModel }) { |
255 | 273 | const deviceProfileId = formModel[DataSourceField.DEVICE_PROFILE_ID]; |
256 | 274 | const transportType = formModel[DataSourceField.TRANSPORT_TYPE]; |
257 | - if (isEdit && ![deviceProfileId, transportType].every(Boolean)) return {}; | |
275 | + if (isEdit && ![deviceProfileId, transportType].every(Boolean)) | |
276 | + return { placeholder: '请选择属性', getPopupContainer: () => document.body }; | |
258 | 277 | return { |
259 | 278 | api: async () => { |
260 | 279 | try { | ... | ... |
... | ... | @@ -126,7 +126,7 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) { |
126 | 126 | const { subscriptionId, data = {} } = res; |
127 | 127 | if (isNullAndUnDef(subscriptionId)) return; |
128 | 128 | const mappingRecord = cmdIdMapping.get(subscriptionId); |
129 | - if (!mappingRecord) return; | |
129 | + if (!mappingRecord || !data) return; | |
130 | 130 | mappingRecord.forEach((item) => { |
131 | 131 | const { attribute, recordIndex, dataSourceIndex } = item; |
132 | 132 | const [[timespan, value]] = data[attribute]; | ... | ... |
... | ... | @@ -249,17 +249,19 @@ |
249 | 249 | </div> |
250 | 250 | </div> |
251 | 251 | <div class="flex justify-between mt-4 text-sm" style="color: #999"> |
252 | - <div> | |
252 | + <div class="flex min-w-20 mr-3"> | |
253 | 253 | <span> |
254 | 254 | {{ item.viewType === ViewType.PRIVATE_VIEW ? '私有看板' : '公共看板' }} |
255 | 255 | </span> |
256 | 256 | <span v-if="item.viewType === ViewType.PUBLIC_VIEW"> |
257 | 257 | <Tooltip title="点击复制分享链接"> |
258 | - <ShareAltOutlined class="ml-2" @click.stop="handleCopyShareUrl(item)" /> | |
258 | + <ShareAltOutlined class="ml-1" @click.stop="handleCopyShareUrl(item)" /> | |
259 | 259 | </Tooltip> |
260 | 260 | </span> |
261 | 261 | </div> |
262 | - <div>{{ item.createTime }}</div> | |
262 | + <Tooltip placement="topLeft" :title="item.createTime"> | |
263 | + <div class="truncate">{{ item.createTime }}</div> | |
264 | + </Tooltip> | |
263 | 265 | </div> |
264 | 266 | </section> |
265 | 267 | </Card> | ... | ... |
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 | +} | ... | ... |