Commit cbb36837759d57efcd0c5004afb3044819d3ab53
Merge remote-tracking branch 'origin/ww'
# Conflicts: # .env.development
Showing
52 changed files
with
3196 additions
and
89 deletions
@@ -84,6 +84,7 @@ | @@ -84,6 +84,7 @@ | ||
84 | "@types/inquirer": "^7.3.3", | 84 | "@types/inquirer": "^7.3.3", |
85 | "@types/intro.js": "^3.0.2", | 85 | "@types/intro.js": "^3.0.2", |
86 | "@types/jest": "^27.0.1", | 86 | "@types/jest": "^27.0.1", |
87 | + "@types/jsoneditor": "^9.9.0", | ||
87 | "@types/lodash-es": "^4.17.4", | 88 | "@types/lodash-es": "^4.17.4", |
88 | "@types/mockjs": "^1.0.4", | 89 | "@types/mockjs": "^1.0.4", |
89 | "@types/node": "^16.6.1", | 90 | "@types/node": "^16.6.1", |
1 | +import { DeviceRecord } from '../device/model/deviceModel'; | ||
1 | import { | 2 | import { |
2 | AddDataBoardParams, | 3 | AddDataBoardParams, |
3 | AddDataComponentParams, | 4 | AddDataComponentParams, |
@@ -183,7 +184,7 @@ export const getAllDeviceByOrg = (organizationId: string, deviceProfileId?: stri | @@ -183,7 +184,7 @@ export const getAllDeviceByOrg = (organizationId: string, deviceProfileId?: stri | ||
183 | * @returns | 184 | * @returns |
184 | */ | 185 | */ |
185 | export const getMeetTheConditionsDevice = (params: GetMeetTheConditionsDeviceParams) => { | 186 | export const getMeetTheConditionsDevice = (params: GetMeetTheConditionsDeviceParams) => { |
186 | - return defHttp.get({ | 187 | + return defHttp.get<DeviceRecord[]>({ |
187 | url: DeviceUrl.GET_DEVICE, | 188 | url: DeviceUrl.GET_DEVICE, |
188 | params, | 189 | params, |
189 | }); | 190 | }); |
@@ -40,6 +40,11 @@ enum DeviceManagerApi { | @@ -40,6 +40,11 @@ enum DeviceManagerApi { | ||
40 | DEVICE_PUBLIC = '/customer/public/device', | 40 | DEVICE_PUBLIC = '/customer/public/device', |
41 | 41 | ||
42 | DEVICE_PRIVATE = '/customer/device', | 42 | DEVICE_PRIVATE = '/customer/device', |
43 | + | ||
44 | + /** | ||
45 | + * @description 通过设备列表获取设备信息 | ||
46 | + */ | ||
47 | + QUERY_DEVICES = '/device/get/devices', | ||
43 | } | 48 | } |
44 | 49 | ||
45 | export const devicePage = (params: DeviceQueryParam) => { | 50 | export const devicePage = (params: DeviceQueryParam) => { |
@@ -330,3 +335,10 @@ export const privateDevice = (tbDeviceId: string) => { | @@ -330,3 +335,10 @@ export const privateDevice = (tbDeviceId: string) => { | ||
330 | { joinPrefix: false } | 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,6 +2,7 @@ import { withInstall } from '/@/utils'; | ||
2 | // @ts-ignore | 2 | // @ts-ignore |
3 | import codeEditor from './src/CodeEditor.vue'; | 3 | import codeEditor from './src/CodeEditor.vue'; |
4 | import jsonPreview from './src/json-preview/JsonPreview.vue'; | 4 | import jsonPreview from './src/json-preview/JsonPreview.vue'; |
5 | +export { JSONEditor } from './src/JSONEditor'; | ||
5 | 6 | ||
6 | export const CodeEditor = withInstall(codeEditor); | 7 | export const CodeEditor = withInstall(codeEditor); |
7 | export const JsonPreview = withInstall(jsonPreview); | 8 | export const JsonPreview = withInstall(jsonPreview); |
1 | +import { Rule } from '/@/components/Form'; | ||
2 | + | ||
3 | +export { default as JSONEditor } from './index.vue'; | ||
4 | + | ||
5 | +export const parseStringToJSON = <T = Recordable>(value: string) => { | ||
6 | + try { | ||
7 | + const json = JSON.parse(value) as T; | ||
8 | + return { json, valid: true }; | ||
9 | + } catch (error) { | ||
10 | + return { json: null, valid: false }; | ||
11 | + } | ||
12 | +}; | ||
13 | + | ||
14 | +export const JSONEditorValidator = (message = 'json格式校验失败'): Rule[] => { | ||
15 | + return [ | ||
16 | + { | ||
17 | + validateTrigger: 'blur', | ||
18 | + validator(_rule: Rule, value: any, _callback: Fn) { | ||
19 | + const { valid } = parseStringToJSON(value); | ||
20 | + if (valid) { | ||
21 | + return Promise.resolve(); | ||
22 | + } | ||
23 | + return Promise.reject(message); | ||
24 | + }, | ||
25 | + }, | ||
26 | + ]; | ||
27 | +}; |
1 | +<script lang="ts" setup> | ||
2 | + import { ref } from 'vue'; | ||
3 | + import JSONEditor, { JSONEditorOptions } from 'jsoneditor'; | ||
4 | + import 'jsoneditor/dist/jsoneditor.min.css'; | ||
5 | + import { unref } from 'vue'; | ||
6 | + import { onMounted } from 'vue'; | ||
7 | + import { computed } from '@vue/reactivity'; | ||
8 | + import { onUnmounted } from 'vue'; | ||
9 | + | ||
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,4 +122,9 @@ export type ComponentType = | ||
122 | | 'ApiSelectScrollLoad' | 122 | | 'ApiSelectScrollLoad' |
123 | | 'TransferModal' | 123 | | 'TransferModal' |
124 | | 'TransferTableModal' | 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,6 +51,9 @@ | ||
51 | <TabPane key="eventManage" tab="事件管理"> | 51 | <TabPane key="eventManage" tab="事件管理"> |
52 | <EventManage :tbDeviceId="deviceDetail.tbDeviceId" /> | 52 | <EventManage :tbDeviceId="deviceDetail.tbDeviceId" /> |
53 | </TabPane> | 53 | </TabPane> |
54 | + <TabPane key="task" tab="任务"> | ||
55 | + <Task :tbDeviceId="deviceDetail.tbDeviceId" /> | ||
56 | + </TabPane> | ||
54 | </Tabs> | 57 | </Tabs> |
55 | </BasicDrawer> | 58 | </BasicDrawer> |
56 | </template> | 59 | </template> |
@@ -71,6 +74,7 @@ | @@ -71,6 +74,7 @@ | ||
71 | import ModelOfMatter from '../tabs/ModelOfMatter.vue'; | 74 | import ModelOfMatter from '../tabs/ModelOfMatter.vue'; |
72 | import EventManage from '../tabs/EventManage/index.vue'; | 75 | import EventManage from '../tabs/EventManage/index.vue'; |
73 | import { DeviceRecord } from '/@/api/device/model/deviceModel'; | 76 | import { DeviceRecord } from '/@/api/device/model/deviceModel'; |
77 | + import Task from '../tabs/Task.vue'; | ||
74 | 78 | ||
75 | export default defineComponent({ | 79 | export default defineComponent({ |
76 | name: 'DeviceModal', | 80 | name: 'DeviceModal', |
@@ -88,6 +92,7 @@ | @@ -88,6 +92,7 @@ | ||
88 | ModelOfMatter, | 92 | ModelOfMatter, |
89 | CommandRecord, | 93 | CommandRecord, |
90 | EventManage, | 94 | EventManage, |
95 | + Task, | ||
91 | }, | 96 | }, |
92 | emits: ['reload', 'register', 'openTbDeviceDetail', 'openGatewayDeviceDetail'], | 97 | emits: ['reload', 'register', 'openTbDeviceDetail', 'openGatewayDeviceDetail'], |
93 | setup(_props, { emit }) { | 98 | setup(_props, { emit }) { |
src/views/device/list/cpns/tabs/Task.vue
0 → 100644
1 | +<script lang="ts" setup> | ||
2 | + import { ReloadOutlined } from '@ant-design/icons-vue'; | ||
3 | + import { Button, List, Tooltip } from 'ant-design-vue'; | ||
4 | + import { TaskCard } from '/@/views/task/center/components/TaskCard'; | ||
5 | + import { reactive, ref, unref } from 'vue'; | ||
6 | + import { PageWrapper } from '/@/components/Page'; | ||
7 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
8 | + import { formSchemas } from '/@/views/task/center/config'; | ||
9 | + import { getTaskCenterList } from '/@/api/task'; | ||
10 | + import { onMounted } from 'vue'; | ||
11 | + import { getBoundingClientRect } from '/@/utils/domUtils'; | ||
12 | + import { TaskRecordType } from '/@/api/task/model'; | ||
13 | + | ||
14 | + 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,6 +9,14 @@ export enum EJobGroup { | ||
9 | DEFAULT = 'DEFAULT', | 9 | DEFAULT = 'DEFAULT', |
10 | SYSTEM = 'SYSTEM', | 10 | SYSTEM = 'SYSTEM', |
11 | REPORT = 'REPORT', | 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,12 +24,8 @@ export const personSchema: DescItem[] = [ | ||
16 | { | 24 | { |
17 | field: 'jobGroup', | 25 | field: 'jobGroup', |
18 | label: '任务分组:', | 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,8 +112,8 @@ export const columnSchedue: BasicColumn[] = [ | ||
108 | const status = record.status; | 112 | const status = record.status; |
109 | const success = status === 1; | 113 | const success = status === 1; |
110 | const color = success ? 'green' : 'red'; | 114 | const color = success ? 'green' : 'red'; |
111 | - const successText: string = '成功'; | ||
112 | - const failedText: string = '失败'; | 115 | + const successText = '成功'; |
116 | + const failedText = '失败'; | ||
113 | const text = success ? successText : failedText; | 117 | const text = success ? successText : failedText; |
114 | return h(Tag, { color: color }, () => text); | 118 | return h(Tag, { color: color }, () => text); |
115 | }, | 119 | }, |
1 | import { BasicColumn, FormSchema } from '/@/components/Table'; | 1 | import { BasicColumn, FormSchema } from '/@/components/Table'; |
2 | import type { FormSchema as QFormSchema } from '/@/components/Form/index'; | 2 | import type { FormSchema as QFormSchema } from '/@/components/Form/index'; |
3 | import { JCronValidator } from '/@/components/Form'; | 3 | import { JCronValidator } from '/@/components/Form'; |
4 | -import { EJobGroup } from './config.data'; | 4 | +import { EJobGroup, EJobGroupName } from './config.data'; |
5 | 5 | ||
6 | // 定时任务表格配置 | 6 | // 定时任务表格配置 |
7 | export const columnSchedue: BasicColumn[] = [ | 7 | export const columnSchedue: BasicColumn[] = [ |
@@ -14,12 +14,8 @@ export const columnSchedue: BasicColumn[] = [ | @@ -14,12 +14,8 @@ export const columnSchedue: BasicColumn[] = [ | ||
14 | title: '任务组名', | 14 | title: '任务组名', |
15 | dataIndex: 'jobGroup', | 15 | dataIndex: 'jobGroup', |
16 | width: 120, | 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,6 +109,7 @@ export const formSchema: QFormSchema[] = [ | ||
113 | field: 'jobGroup', | 109 | field: 'jobGroup', |
114 | component: 'Select', | 110 | component: 'Select', |
115 | label: '任务分组', | 111 | label: '任务分组', |
112 | + dynamicDisabled: true, | ||
116 | colProps: { | 113 | colProps: { |
117 | span: 24, | 114 | span: 24, |
118 | }, | 115 | }, |
@@ -120,17 +117,21 @@ export const formSchema: QFormSchema[] = [ | @@ -120,17 +117,21 @@ export const formSchema: QFormSchema[] = [ | ||
120 | placeholder: '请选择任务分组', | 117 | placeholder: '请选择任务分组', |
121 | options: [ | 118 | options: [ |
122 | { | 119 | { |
123 | - label: '默认', | 120 | + label: EJobGroupName.DEFAULT, |
124 | value: EJobGroup.DEFAULT, | 121 | value: EJobGroup.DEFAULT, |
125 | }, | 122 | }, |
126 | { | 123 | { |
127 | - label: '系统', | 124 | + label: EJobGroupName.SYSTEM, |
128 | value: EJobGroup.SYSTEM, | 125 | value: EJobGroup.SYSTEM, |
129 | }, | 126 | }, |
130 | { | 127 | { |
131 | - label: '报表', | 128 | + label: EJobGroupName.REPORT, |
132 | value: EJobGroup.REPORT, | 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,6 +154,7 @@ export const formSchema: QFormSchema[] = [ | ||
153 | label: 'Cron表达式', | 154 | label: 'Cron表达式', |
154 | component: 'JEasyCron', | 155 | component: 'JEasyCron', |
155 | defaultValue: '* * * * * ? *', | 156 | defaultValue: '* * * * * ? *', |
157 | + dynamicDisabled: true, | ||
156 | colProps: { | 158 | colProps: { |
157 | span: 24, | 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 | <script lang="ts"> | 1 | <script lang="ts"> |
2 | export default { | 2 | export default { |
3 | + components: { Spin }, | ||
3 | inheritAttrs: false, | 4 | inheritAttrs: false, |
4 | }; | 5 | }; |
5 | </script> | 6 | </script> |
6 | <script lang="ts" setup> | 7 | <script lang="ts" setup> |
8 | + import { Spin } from 'ant-design-vue'; | ||
7 | import { RadioRecord } from '../../detail/config/util'; | 9 | import { RadioRecord } from '../../detail/config/util'; |
8 | import { ControlComponentDefaultConfig, ControlComponentValue } from './control.config'; | 10 | import { ControlComponentDefaultConfig, ControlComponentValue } from './control.config'; |
9 | import { useSendCommand } from './useSendCommand'; | 11 | import { useSendCommand } from './useSendCommand'; |
12 | + import { ref } from 'vue'; | ||
10 | 13 | ||
11 | interface VisualComponentProps<Layout = Recordable, Value = ControlComponentValue> { | 14 | interface VisualComponentProps<Layout = Recordable, Value = ControlComponentValue> { |
12 | value?: Value; | 15 | value?: Value; |
@@ -23,33 +26,45 @@ | @@ -23,33 +26,45 @@ | ||
23 | const emit = defineEmits(['update:value', 'change']); | 26 | const emit = defineEmits(['update:value', 'change']); |
24 | 27 | ||
25 | const { sendCommand } = useSendCommand(); | 28 | const { sendCommand } = useSendCommand(); |
26 | - const handleChange = (event: Event) => { | 29 | + |
30 | + const loading = ref(false); | ||
31 | + const handleChange = async (event: Event) => { | ||
27 | const _value = (event.target as HTMLInputElement).checked; | 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 | emit('update:value', _value); | 42 | emit('update:value', _value); |
29 | emit('change', _value); | 43 | emit('change', _value); |
30 | - sendCommand(props.value!, _value); | ||
31 | }; | 44 | }; |
32 | </script> | 45 | </script> |
33 | 46 | ||
34 | <template> | 47 | <template> |
35 | <div class="flex flex-col justify-center"> | 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 | </div> | 68 | </div> |
54 | </template> | 69 | </template> |
55 | 70 |
@@ -31,8 +31,14 @@ | @@ -31,8 +31,14 @@ | ||
31 | const checked = ref(!!Number(props.value.value)); | 31 | const checked = ref(!!Number(props.value.value)); |
32 | 32 | ||
33 | const { sendCommand } = useSendCommand(); | 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 | watchEffect(() => { | 44 | watchEffect(() => { |
@@ -59,6 +65,6 @@ | @@ -59,6 +65,6 @@ | ||
59 | {{ props.value.attributeRename || props.value.attribute }} | 65 | {{ props.value.attributeRename || props.value.attribute }} |
60 | </span> | 66 | </span> |
61 | </div> | 67 | </div> |
62 | - <Switch v-model:checked="checked" @change="handleChange" /> | 68 | + <Switch v-model:checked="checked" :loading="loading" @change="handleChange" /> |
63 | </div> | 69 | </div> |
64 | </template> | 70 | </template> |
1 | <script lang="ts"> | 1 | <script lang="ts"> |
2 | export default { | 2 | export default { |
3 | + components: { Spin }, | ||
3 | inheritAttrs: false, | 4 | inheritAttrs: false, |
4 | }; | 5 | }; |
5 | </script> | 6 | </script> |
@@ -8,6 +9,8 @@ | @@ -8,6 +9,8 @@ | ||
8 | import { DEFAULT_RADIO_RECORD, fontSize, RadioRecord } from '../../detail/config/util'; | 9 | import { DEFAULT_RADIO_RECORD, fontSize, RadioRecord } from '../../detail/config/util'; |
9 | import { ControlComponentDefaultConfig, ControlComponentValue } from './control.config'; | 10 | import { ControlComponentDefaultConfig, ControlComponentValue } from './control.config'; |
10 | import { useSendCommand } from './useSendCommand'; | 11 | import { useSendCommand } from './useSendCommand'; |
12 | + import { ref } from 'vue'; | ||
13 | + import { Spin } from 'ant-design-vue'; | ||
11 | 14 | ||
12 | const props = defineProps<{ | 15 | const props = defineProps<{ |
13 | value?: ControlComponentValue; | 16 | value?: ControlComponentValue; |
@@ -18,11 +21,20 @@ | @@ -18,11 +21,20 @@ | ||
18 | const emit = defineEmits(['update:value', 'change']); | 21 | const emit = defineEmits(['update:value', 'change']); |
19 | 22 | ||
20 | const { sendCommand } = useSendCommand(); | 23 | const { sendCommand } = useSendCommand(); |
21 | - const handleChange = (event: Event) => { | 24 | + const loading = ref(false); |
25 | + const handleChange = async (event: Event) => { | ||
22 | const _value = (event.target as HTMLInputElement).checked; | 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 | emit('update:value', _value); | 36 | emit('update:value', _value); |
24 | emit('change', _value); | 37 | emit('change', _value); |
25 | - sendCommand(props.value!, _value); | ||
26 | }; | 38 | }; |
27 | 39 | ||
28 | const getRadio = computed(() => { | 40 | const getRadio = computed(() => { |
@@ -32,35 +44,37 @@ | @@ -32,35 +44,37 @@ | ||
32 | 44 | ||
33 | <template> | 45 | <template> |
34 | <div class="flex flex-col"> | 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 | </div> | 78 | </div> |
65 | </template> | 79 | </template> |
66 | 80 |
@@ -5,33 +5,44 @@ import { DeviceTypeEnum } from '/@/api/device/model/deviceModel'; | @@ -5,33 +5,44 @@ import { DeviceTypeEnum } from '/@/api/device/model/deviceModel'; | ||
5 | import { getModelServices } from '/@/api/device/modelOfMatter'; | 5 | import { getModelServices } from '/@/api/device/modelOfMatter'; |
6 | import { useMessage } from '/@/hooks/web/useMessage'; | 6 | import { useMessage } from '/@/hooks/web/useMessage'; |
7 | import { isString } from '/@/utils/is'; | 7 | import { isString } from '/@/utils/is'; |
8 | +import { TransportTypeEnum } from '/@/views/device/profiles/components/TransportDescript/const'; | ||
8 | 9 | ||
9 | const { createMessage } = useMessage(); | 10 | const { createMessage } = useMessage(); |
10 | export function useSendCommand() { | 11 | export function useSendCommand() { |
12 | + const error = () => { | ||
13 | + createMessage.error('下发指令失败'); | ||
14 | + return false; | ||
15 | + }; | ||
11 | const sendCommand = async (record: ControlComponentValue, value: any) => { | 16 | const sendCommand = async (record: ControlComponentValue, value: any) => { |
12 | - if (!record) return; | 17 | + if (!record) return error(); |
13 | const { deviceProfileId, attribute, deviceType } = record; | 18 | const { deviceProfileId, attribute, deviceType } = record; |
14 | let { deviceId } = record; | 19 | let { deviceId } = record; |
15 | - if (!deviceId) return; | 20 | + if (!deviceId) return error(); |
16 | try { | 21 | try { |
17 | const list = await getDeviceProfile(); | 22 | const list = await getDeviceProfile(); |
18 | const deviceProfile = list.find((item) => item.id === deviceProfileId); | 23 | const deviceProfile = list.find((item) => item.id === deviceProfileId); |
19 | - if (!deviceProfile) return; | 24 | + if (!deviceProfile) return error(); |
25 | + | ||
20 | let params: string | Recordable = { | 26 | let params: string | Recordable = { |
21 | [attribute!]: Number(value), | 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 | const record = serviceList.find((item) => item.identifier === attribute); | 33 | const record = serviceList.find((item) => item.identifier === attribute); |
26 | const sendCommand = record?.functionJson.inputData?.at(0)?.serviceCommand || ''; | 34 | const sendCommand = record?.functionJson.inputData?.at(0)?.serviceCommand || ''; |
27 | params = isString(sendCommand) ? sendCommand : JSON.stringify(sendCommand); | 35 | params = isString(sendCommand) ? sendCommand : JSON.stringify(sendCommand); |
28 | } | 36 | } |
37 | + | ||
29 | if (deviceType === DeviceTypeEnum.SENSOR) { | 38 | if (deviceType === DeviceTypeEnum.SENSOR) { |
30 | deviceId = await getDeviceRelation({ | 39 | deviceId = await getDeviceRelation({ |
31 | deviceId, | 40 | deviceId, |
32 | isSlave: deviceType === DeviceTypeEnum.SENSOR, | 41 | isSlave: deviceType === DeviceTypeEnum.SENSOR, |
33 | }); | 42 | }); |
34 | } | 43 | } |
44 | + | ||
45 | + // 控制按钮下发命令为0 或 1 | ||
35 | await sendCommandOneway({ | 46 | await sendCommandOneway({ |
36 | deviceId, | 47 | deviceId, |
37 | value: { | 48 | value: { |
@@ -44,7 +55,11 @@ export function useSendCommand() { | @@ -44,7 +55,11 @@ export function useSendCommand() { | ||
44 | }, | 55 | }, |
45 | }); | 56 | }); |
46 | createMessage.success('命令下发成功'); | 57 | createMessage.success('命令下发成功'); |
47 | - } catch (error) {} | 58 | + } catch (msg) { |
59 | + return error(); | ||
60 | + } finally { | ||
61 | + return true; | ||
62 | + } | ||
48 | }; | 63 | }; |
49 | return { | 64 | return { |
50 | sendCommand, | 65 | sendCommand, |
@@ -57,6 +57,13 @@ | @@ -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 | const validate = async () => { | 67 | const validate = async () => { |
61 | await basicMethod.validate(); | 68 | await basicMethod.validate(); |
62 | await validateDataSourceField(); | 69 | await validateDataSourceField(); |
@@ -258,6 +265,16 @@ | @@ -258,6 +265,16 @@ | ||
258 | return isControlComponent(props.frontId as FrontComponent); | 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 | onMounted(() => handleSort()); | 278 | onMounted(() => handleSort()); |
262 | 279 | ||
263 | defineExpose({ | 280 | defineExpose({ |
@@ -293,7 +310,7 @@ | @@ -293,7 +310,7 @@ | ||
293 | 310 | ||
294 | <section ref="formListEl"> | 311 | <section ref="formListEl"> |
295 | <div v-for="item in dataSource" :data-id="item.id" :key="item.id" class="flex bg-light-50"> | 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 | <div class="pl-2 flex-auto"> | 314 | <div class="pl-2 flex-auto"> |
298 | <component | 315 | <component |
299 | :frontId="$props.frontId" | 316 | :frontId="$props.frontId" |
@@ -9,6 +9,8 @@ import { getModelServices } from '/@/api/device/modelOfMatter'; | @@ -9,6 +9,8 @@ import { getModelServices } from '/@/api/device/modelOfMatter'; | ||
9 | import { findDictItemByCode } from '/@/api/system/dict'; | 9 | import { findDictItemByCode } from '/@/api/system/dict'; |
10 | import { DeviceTypeEnum } from '/@/api/device/model/deviceModel'; | 10 | import { DeviceTypeEnum } from '/@/api/device/model/deviceModel'; |
11 | import { DataTypeEnum } from '/@/components/Form/src/externalCompns/components/StructForm/config'; | 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 | export enum BasicConfigField { | 15 | export enum BasicConfigField { |
14 | NAME = 'name', | 16 | NAME = 'name', |
@@ -66,7 +68,7 @@ export const isMapComponent = (frontId: FrontComponent) => { | @@ -66,7 +68,7 @@ export const isMapComponent = (frontId: FrontComponent) => { | ||
66 | }; | 68 | }; |
67 | 69 | ||
68 | const isTcpProfile = (transportType: string) => { | 70 | const isTcpProfile = (transportType: string) => { |
69 | - return transportType === 'TCP'; | 71 | + return transportType === TransportTypeEnum.TCP; |
70 | }; | 72 | }; |
71 | 73 | ||
72 | export const basicSchema: FormSchema[] = [ | 74 | export const basicSchema: FormSchema[] = [ |
@@ -117,6 +119,7 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For | @@ -117,6 +119,7 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For | ||
117 | component: 'ApiSelect', | 119 | component: 'ApiSelect', |
118 | label: '设备类型', | 120 | label: '设备类型', |
119 | colProps: { span: 8 }, | 121 | colProps: { span: 8 }, |
122 | + rules: [{ message: '请选择设备类型', required: true }], | ||
120 | // defaultValue: DeviceTypeEnum.SENSOR, | 123 | // defaultValue: DeviceTypeEnum.SENSOR, |
121 | componentProps: ({ formActionType }) => { | 124 | componentProps: ({ formActionType }) => { |
122 | const { setFieldsValue } = formActionType; | 125 | const { setFieldsValue } = formActionType; |
@@ -180,7 +183,7 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For | @@ -180,7 +183,7 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For | ||
180 | colProps: { span: 8 }, | 183 | colProps: { span: 8 }, |
181 | rules: [{ required: true, message: '组织为必填项' }], | 184 | rules: [{ required: true, message: '组织为必填项' }], |
182 | componentProps({ formActionType }) { | 185 | componentProps({ formActionType }) { |
183 | - const { setFieldsValue } = formActionType; | 186 | + const { setFieldsValue, getFieldsValue } = formActionType; |
184 | return { | 187 | return { |
185 | placeholder: '请选择组织', | 188 | placeholder: '请选择组织', |
186 | api: async () => { | 189 | api: async () => { |
@@ -192,6 +195,9 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For | @@ -192,6 +195,9 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For | ||
192 | setFieldsValue({ | 195 | setFieldsValue({ |
193 | [DataSourceField.DEVICE_ID]: null, | 196 | [DataSourceField.DEVICE_ID]: null, |
194 | }); | 197 | }); |
198 | + nextTick(() => { | ||
199 | + console.log('org change', getFieldsValue()); | ||
200 | + }); | ||
195 | }, | 201 | }, |
196 | getPopupContainer: () => document.body, | 202 | getPopupContainer: () => document.body, |
197 | }; | 203 | }; |
@@ -250,11 +256,24 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For | @@ -250,11 +256,24 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For | ||
250 | component: 'ApiSelect', | 256 | component: 'ApiSelect', |
251 | label: '属性', | 257 | label: '属性', |
252 | colProps: { span: 8 }, | 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 | componentProps({ formModel }) { | 272 | componentProps({ formModel }) { |
255 | const deviceProfileId = formModel[DataSourceField.DEVICE_PROFILE_ID]; | 273 | const deviceProfileId = formModel[DataSourceField.DEVICE_PROFILE_ID]; |
256 | const transportType = formModel[DataSourceField.TRANSPORT_TYPE]; | 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 | return { | 277 | return { |
259 | api: async () => { | 278 | api: async () => { |
260 | try { | 279 | try { |
@@ -57,7 +57,7 @@ | @@ -57,7 +57,7 @@ | ||
57 | const getAceClass = computed((): string => userStore.getDarkMode); | 57 | const getAceClass = computed((): string => userStore.getDarkMode); |
58 | 58 | ||
59 | const props = defineProps<{ | 59 | const props = defineProps<{ |
60 | - value: Recordable; | 60 | + value?: Recordable; |
61 | }>(); | 61 | }>(); |
62 | 62 | ||
63 | const ROUTE = useRoute(); | 63 | const ROUTE = useRoute(); |
@@ -126,7 +126,7 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) { | @@ -126,7 +126,7 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) { | ||
126 | const { subscriptionId, data = {} } = res; | 126 | const { subscriptionId, data = {} } = res; |
127 | if (isNullAndUnDef(subscriptionId)) return; | 127 | if (isNullAndUnDef(subscriptionId)) return; |
128 | const mappingRecord = cmdIdMapping.get(subscriptionId); | 128 | const mappingRecord = cmdIdMapping.get(subscriptionId); |
129 | - if (!mappingRecord) return; | 129 | + if (!mappingRecord || !data) return; |
130 | mappingRecord.forEach((item) => { | 130 | mappingRecord.forEach((item) => { |
131 | const { attribute, recordIndex, dataSourceIndex } = item; | 131 | const { attribute, recordIndex, dataSourceIndex } = item; |
132 | const [[timespan, value]] = data[attribute]; | 132 | const [[timespan, value]] = data[attribute]; |
@@ -249,17 +249,19 @@ | @@ -249,17 +249,19 @@ | ||
249 | </div> | 249 | </div> |
250 | </div> | 250 | </div> |
251 | <div class="flex justify-between mt-4 text-sm" style="color: #999"> | 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 | <span> | 253 | <span> |
254 | {{ item.viewType === ViewType.PRIVATE_VIEW ? '私有看板' : '公共看板' }} | 254 | {{ item.viewType === ViewType.PRIVATE_VIEW ? '私有看板' : '公共看板' }} |
255 | </span> | 255 | </span> |
256 | <span v-if="item.viewType === ViewType.PUBLIC_VIEW"> | 256 | <span v-if="item.viewType === ViewType.PUBLIC_VIEW"> |
257 | <Tooltip title="点击复制分享链接"> | 257 | <Tooltip title="点击复制分享链接"> |
258 | - <ShareAltOutlined class="ml-2" @click.stop="handleCopyShareUrl(item)" /> | 258 | + <ShareAltOutlined class="ml-1" @click.stop="handleCopyShareUrl(item)" /> |
259 | </Tooltip> | 259 | </Tooltip> |
260 | </span> | 260 | </span> |
261 | </div> | 261 | </div> |
262 | - <div>{{ item.createTime }}</div> | 262 | + <Tooltip placement="topLeft" :title="item.createTime"> |
263 | + <div class="truncate">{{ item.createTime }}</div> | ||
264 | + </Tooltip> | ||
263 | </div> | 265 | </div> |
264 | </section> | 266 | </section> |
265 | </Card> | 267 | </Card> |
1 | import type { ComputedRef, Ref } from 'vue'; | 1 | import type { ComputedRef, Ref } from 'vue'; |
2 | +import { DataActionModeEnum } from '/@/enums/toolEnum'; | ||
2 | 3 | ||
3 | export type DynamicProps<T> = { | 4 | export type DynamicProps<T> = { |
4 | [P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>; | 5 | [P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>; |
5 | }; | 6 | }; |
7 | + | ||
8 | +export interface ModalParamsType<T = Recordable> { | ||
9 | + mode: DataActionModeEnum; | ||
10 | + record: T; | ||
11 | +} |