Commit 45d95317ee1ee1f32ecb0f297ce2b006d9e03d5c
1 parent
f58124e6
feat: 完成任务中心modbus指令生成&&设备详情任务是否允许任务执行
Showing
17 changed files
with
304 additions
and
101 deletions
... | ... | @@ -14,9 +14,9 @@ VITE_GLOB_PUBLIC_PATH = / |
14 | 14 | # VITE_PROXY = [["/api","http://101.133.234.90:8080/api"]] |
15 | 15 | # 线上测试环境 |
16 | 16 | # VITE_PROXY = [["/api","http://localhost:8080/api"],["/thingskit-drawio","http://localhost:3000/"]] |
17 | -VITE_PROXY = [["/api","http://222.180.200.114:48080/api"],["/thingskit-drawio","http://localhost:3000/"],["/large-designer", "http://localhost:5555/large-designer/"]] | |
17 | +# VITE_PROXY = [["/api","http://222.180.200.114:48080/api"],["/thingskit-drawio","http://localhost:3000/"],["/large-designer", "http://localhost:5555/large-designer/"]] | |
18 | 18 | # VITE_PROXY = [["/api","http://121.37.251.8:8080/api"],["/thingskit-drawio","http://localhost:3000/"]] |
19 | -# VITE_PROXY = [["/api","http://192.168.10.125:8080/api"],["/thingskit-drawio","http://192.168.10.125:8080/api"]] | |
19 | +VITE_PROXY = [["/api","http://192.168.10.114:8080/api"],["/thingskit-drawio","http://192.168.10.125:8080/api"]] | |
20 | 20 | |
21 | 21 | # 实时数据的ws地址 |
22 | 22 | # VITE_WEB_SOCKET = ws://localhost:8080/api/ws/plugins/telemetry?token= | ... | ... |
1 | -import { CreateTaskRecordType, GetTaskListParamsType, TaskRecordType } from './model'; | |
1 | +import { | |
2 | + CreateTaskRecordType, | |
3 | + GenModbusCommandType, | |
4 | + GetTaskListParamsType, | |
5 | + TaskRecordType, | |
6 | +} from './model'; | |
2 | 7 | import { PaginationResult } from '/#/axios'; |
3 | 8 | import { defHttp } from '/@/utils/http/axios'; |
4 | 9 | |
... | ... | @@ -9,6 +14,8 @@ enum Api { |
9 | 14 | DELETE_TASK = '/task_center', |
10 | 15 | UPDATE_TASK = '/task_center/update', |
11 | 16 | CANCEL_TASK = '/task_center', |
17 | + | |
18 | + GEN_MODBUS_COMMAND = '/js/modbus', | |
12 | 19 | } |
13 | 20 | |
14 | 21 | export const getTaskCenterList = (params: GetTaskListParamsType) => { |
... | ... | @@ -45,8 +52,27 @@ export const updateTask = (data: CreateTaskRecordType & Record<'id', string>) => |
45 | 52 | }); |
46 | 53 | }; |
47 | 54 | |
48 | -export const cancelTask = (data: Record<'id' | 'tbDeviceId', string>) => { | |
55 | +/** | |
56 | + * @description 取消任务 | |
57 | + * @param data | |
58 | + * @returns | |
59 | + */ | |
60 | +export const cancelTask = ( | |
61 | + data: Record<'id' | 'tbDeviceId', string> & Record<'allow', boolean> | |
62 | +) => { | |
49 | 63 | return defHttp.put({ |
50 | - url: `${Api.CANCEL_TASK}/${data.id}/cancel/${data.tbDeviceId}`, | |
64 | + url: `${Api.CANCEL_TASK}/${data.id}/update/${data.tbDeviceId}/${data.allow}`, | |
65 | + }); | |
66 | +}; | |
67 | + | |
68 | +/** | |
69 | + * @description 生成modbus指令 | |
70 | + * @param data | |
71 | + * @returns {string} | |
72 | + */ | |
73 | +export const genModbusCommand = (data: GenModbusCommandType) => { | |
74 | + return defHttp.post<string>({ | |
75 | + url: Api.GEN_MODBUS_COMMAND, | |
76 | + data, | |
51 | 77 | }); |
52 | 78 | }; | ... | ... |
... | ... | @@ -3,13 +3,14 @@ import { |
3 | 3 | ExecuteTimeTypeEnum, |
4 | 4 | PeriodTypeEnum, |
5 | 5 | PushWayEnum, |
6 | -} from '/@/views/task/center/components/CreateModal/config'; | |
6 | +} from '/@/views/task/center/components/DetailModal/config'; | |
7 | 7 | import { TaskTypeEnum } from '/@/views/task/center/config'; |
8 | 8 | |
9 | 9 | export interface GetTaskListParamsType { |
10 | 10 | page: number; |
11 | 11 | pageSize: number; |
12 | 12 | state?: string; |
13 | + tbDeviceId?: string; | |
13 | 14 | } |
14 | 15 | |
15 | 16 | export interface CreateTaskRecordType { |
... | ... | @@ -45,4 +46,19 @@ export interface TaskRecordType extends CreateTaskRecordType { |
45 | 46 | creator: string; |
46 | 47 | enabled: boolean; |
47 | 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[]; | |
48 | 64 | } | ... | ... |
... | ... | @@ -6,7 +6,6 @@ |
6 | 6 | import { onMounted } from 'vue'; |
7 | 7 | import { computed } from '@vue/reactivity'; |
8 | 8 | import { onUnmounted } from 'vue'; |
9 | - import { watch } from 'vue'; | |
10 | 9 | |
11 | 10 | enum EventEnum { |
12 | 11 | UPDATE_VALUE = 'update:value', |
... | ... | @@ -64,15 +63,15 @@ |
64 | 63 | editoreRef.value = new JSONEditor(unref(jsonEditorElRef), unref(getOptions)); |
65 | 64 | }; |
66 | 65 | |
67 | - watch( | |
68 | - () => props.value, | |
69 | - (target) => { | |
70 | - unref(editoreRef)?.setText(target || ''); | |
71 | - }, | |
72 | - { | |
73 | - immediate: true, | |
74 | - } | |
75 | - ); | |
66 | + // watch( | |
67 | + // () => props.value, | |
68 | + // (target) => { | |
69 | + // unref(editoreRef)?.setText(target || ''); | |
70 | + // }, | |
71 | + // { | |
72 | + // immediate: true, | |
73 | + // } | |
74 | + // ); | |
76 | 75 | |
77 | 76 | const get = (): string => { |
78 | 77 | return unref(editoreRef)?.getText() || ''; |
... | ... | @@ -84,6 +83,7 @@ |
84 | 83 | |
85 | 84 | onMounted(() => { |
86 | 85 | initialize(); |
86 | + unref(editoreRef)?.setText(props.value || ''); | |
87 | 87 | }); |
88 | 88 | |
89 | 89 | onUnmounted(() => { | ... | ... |
... | ... | @@ -11,7 +11,7 @@ |
11 | 11 | import { getBoundingClientRect } from '/@/utils/domUtils'; |
12 | 12 | import { TaskRecordType } from '/@/api/task/model'; |
13 | 13 | |
14 | - defineProps<{ | |
14 | + const props = defineProps<{ | |
15 | 15 | tbDeviceId: string; |
16 | 16 | }>(); |
17 | 17 | |
... | ... | @@ -48,6 +48,7 @@ |
48 | 48 | const { items } = await getTaskCenterList({ |
49 | 49 | page: pagination.current, |
50 | 50 | pageSize: pagination.pageSize, |
51 | + tbDeviceId: props.tbDeviceId, | |
51 | 52 | ...pagination.params, |
52 | 53 | }); |
53 | 54 | dataSource.value = items; | ... | ... |
... | ... | @@ -13,6 +13,7 @@ import { getDeviceProfile } from '/@/api/alarm/position'; |
13 | 13 | import { JSONEditorValidator } from '/@/components/CodeEditor/src/JSONEditor'; |
14 | 14 | import { TimeUnitEnum, TimeUnitNameEnum } from '/@/enums/toolEnum'; |
15 | 15 | import { createPickerSearch } from '/@/utils/pickerSearch'; |
16 | +import { dateUtil } from '/@/utils/dateUtil'; | |
16 | 17 | |
17 | 18 | useComponentRegister('DevicePicker', DevicePicker); |
18 | 19 | useComponentRegister('PollCommandInput', PollCommandInput); |
... | ... | @@ -64,9 +65,9 @@ export enum ExecuteTimeTypeNameEnum { |
64 | 65 | } |
65 | 66 | |
66 | 67 | export enum PeriodTypeEnum { |
67 | - MONTH, | |
68 | - WEEK, | |
69 | - DAY, | |
68 | + MONTH = 'MONTH', | |
69 | + WEEK = 'WEEK', | |
70 | + DAY = 'DAY', | |
70 | 71 | } |
71 | 72 | |
72 | 73 | export enum PeriodTypeNameEnum { |
... | ... | @@ -197,7 +198,7 @@ export const formSchemas: FormSchema[] = [ |
197 | 198 | field: FormFieldsEnum.PUSH_WAY, |
198 | 199 | component: 'RadioGroup', |
199 | 200 | label: '推送方式', |
200 | - defaultValue: PushWayEnum.MQTT, | |
201 | + defaultValue: PushWayEnum.TCP, | |
201 | 202 | show: false, |
202 | 203 | componentProps: { |
203 | 204 | options: [ |
... | ... | @@ -317,6 +318,7 @@ export const formSchemas: FormSchema[] = [ |
317 | 318 | componentProps: { |
318 | 319 | getPopupContainer: () => document.body, |
319 | 320 | valueFormat: 'HH:mm:ss', |
321 | + defaultOpenValue: dateUtil('00:00:00', 'HH:mm:ss'), | |
320 | 322 | }, |
321 | 323 | }, |
322 | 324 | { | ... | ... |
... | ... | @@ -6,8 +6,7 @@ import { DeviceCascadePickerValueType } from '../DevicePicker'; |
6 | 6 | import { TaskTargetEnum } from '../../config'; |
7 | 7 | import { TimeUnitEnum } from '/@/enums/toolEnum'; |
8 | 8 | |
9 | -export interface FormValueType | |
10 | - extends Partial<Record<Exclude<FormFieldsEnum, FormFieldsEnum.TRANSPORT_TYPE>, any>> { | |
9 | +export interface FormValueType extends Partial<Record<FormFieldsEnum, any>> { | |
11 | 10 | [FormFieldsEnum.EXECUTE_TARGET_DATA]: DeviceCascadePickerValueType; |
12 | 11 | } |
13 | 12 | |
... | ... | @@ -106,7 +105,7 @@ export const composeData = (result: Required<FormValueType>): CreateTaskRecordTy |
106 | 105 | targetType, |
107 | 106 | executeContent: { |
108 | 107 | pushContent: { |
109 | - rpcCommand: pushWay === PushWayEnum.MQTT ? JSON.parse(rpcCommand) : pushWay, | |
108 | + rpcCommand: pushWay === PushWayEnum.MQTT ? JSON.parse(rpcCommand) : rpcCommand, | |
110 | 109 | }, |
111 | 110 | pushWay, |
112 | 111 | type: executeContentType, |
... | ... | @@ -138,7 +137,8 @@ export const parseData = (result: TaskRecordType): Required<FormValueType> => { |
138 | 137 | return { |
139 | 138 | name, |
140 | 139 | targetType, |
141 | - rpcCommand: pushWay === PushWayEnum.MQTT ? JSON.stringify(rpcCommand) : rpcCommand, | |
140 | + rpcCommand: pushWay === PushWayEnum.MQTT ? JSON.stringify(rpcCommand, null, 2) : rpcCommand, | |
141 | + transportType: pushWay, | |
142 | 142 | pushWay, |
143 | 143 | executeContentType, |
144 | 144 | executeTargetData: { | ... | ... |
src/views/task/center/components/PollCommandInput/GenModbusCommandModal.vue
renamed from
src/views/task/center/components/PollCommandInput/CreateTCPCommandModal.vue
1 | 1 | <script lang="ts" setup> |
2 | - import { Button } from 'ant-design-vue'; | |
2 | + import { Button, Spin } from 'ant-design-vue'; | |
3 | 3 | import { BasicForm, useForm } from '/@/components/Form'; |
4 | 4 | import { BasicModal, useModal } from '/@/components/Modal'; |
5 | 5 | import { formSchemas } from './config'; |
6 | 6 | import { ref } from 'vue'; |
7 | 7 | import { unref } from 'vue'; |
8 | + import { composeModbusModalData } from './util'; | |
9 | + import { ModbusCommandValueType } from './type'; | |
10 | + import { genModbusCommand } from '/@/api/task'; | |
8 | 11 | |
9 | 12 | const emit = defineEmits(['update:value']); |
10 | 13 | |
... | ... | @@ -19,9 +22,29 @@ |
19 | 22 | |
20 | 23 | const commandValue = ref(''); |
21 | 24 | |
25 | + const loading = ref(false); | |
22 | 26 | const handleGetValue = async () => { |
23 | - const value = getFieldsValue(); | |
24 | - console.log(value); | |
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('')}`, ''); | |
25 | 48 | }; |
26 | 49 | |
27 | 50 | const handleOk = () => { |
... | ... | @@ -30,13 +53,22 @@ |
30 | 53 | </script> |
31 | 54 | |
32 | 55 | <template> |
33 | - <BasicModal @register="registerModal" title="配置操作" @ok="handleOk"> | |
56 | + <BasicModal | |
57 | + @register="registerModal" | |
58 | + :okButtonProps="{ loading }" | |
59 | + title="配置操作" | |
60 | + @ok="handleOk" | |
61 | + > | |
34 | 62 | <BasicForm @register="registerForm" class="create-tcp-command-form" /> |
35 | 63 | <section> |
36 | 64 | <Button @click="handleGetValue" type="link" class="!px-0">生成预览</Button> |
37 | - <div v-if="commandValue"> | |
65 | + <div> | |
38 | 66 | <div class="text-gray-400">Modbus 指令预览</div> |
39 | - <code class="bg-dark-50 text-light-50 p-1 w-full block mt-1">{{ commandValue }}</code> | |
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> | |
40 | 72 | </div> |
41 | 73 | </section> |
42 | 74 | </BasicModal> | ... | ... |
1 | 1 | <script lang="ts" setup> |
2 | 2 | import { InputGroup, InputNumber, Select, Input } from 'ant-design-vue'; |
3 | - import { watch } from 'vue'; | |
4 | 3 | import { unref } from 'vue'; |
5 | 4 | import { computed } from 'vue'; |
6 | 5 | import { ref } from 'vue'; |
... | ... | @@ -14,13 +13,13 @@ |
14 | 13 | |
15 | 14 | const DEC_MAX_VALUE = parseInt('0xffff', 16); |
16 | 15 | |
17 | - const props = withDefaults( | |
16 | + withDefaults( | |
18 | 17 | defineProps<{ |
19 | - value?: string; | |
18 | + value?: number | string; | |
20 | 19 | inputProps?: Recordable; |
21 | 20 | }>(), |
22 | 21 | { |
23 | - value: '0', | |
22 | + value: 0, | |
24 | 23 | inputProps: () => ({}), |
25 | 24 | } |
26 | 25 | ); |
... | ... | @@ -44,23 +43,24 @@ |
44 | 43 | return (inputValue.value as number) > DEC_MAX_VALUE ? '0x0000' : formatValue; |
45 | 44 | }); |
46 | 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 | + | |
47 | 54 | const handleEmit = () => { |
48 | - const syncValue = unref(type) === AddressTypeEnum.DEC ? unref(getDecValue) : unref(getHexValue); | |
55 | + const syncValue = toDEC(unref(inputValue)); | |
49 | 56 | emit('update:value', syncValue); |
50 | 57 | }; |
51 | 58 | |
52 | 59 | const handleChange = (value: AddressTypeEnum) => { |
53 | 60 | const syncValue = value === AddressTypeEnum.DEC ? unref(getHexValue) : unref(getDecValue); |
54 | 61 | inputValue.value = syncValue; |
55 | - emit('update:value', syncValue); | |
62 | + emit('update:value', toDEC(syncValue)); | |
56 | 63 | }; |
57 | - | |
58 | - watch( | |
59 | - () => props.value, | |
60 | - (targetValue) => { | |
61 | - inputValue.value = targetValue || 0; | |
62 | - } | |
63 | - ); | |
64 | 64 | </script> |
65 | 65 | |
66 | 66 | <template> |
... | ... | @@ -76,10 +76,10 @@ |
76 | 76 | v-model:value="inputValue" |
77 | 77 | :step="1" |
78 | 78 | class="flex-1" |
79 | - @change="handleEmit" | |
80 | 79 | v-bind="inputProps" |
80 | + @change="handleEmit" | |
81 | 81 | /> |
82 | - <Input v-if="type === AddressTypeEnum.HEX" v-model:value="inputValue" /> | |
82 | + <Input v-if="type === AddressTypeEnum.HEX" v-model:value="inputValue" @change="handleEmit" /> | |
83 | 83 | <div class="text-center h-8 leading-8 px-2 bg-gray-200 cursor-pointer rounded-1 w-20"> |
84 | 84 | <div v-if="type === AddressTypeEnum.DEC">{{ getDecValue }}</div> |
85 | 85 | <div v-if="type === AddressTypeEnum.HEX">{{ getHexValue }}</div> | ... | ... |
... | ... | @@ -6,10 +6,14 @@ import { createPickerSearch } from '/@/utils/pickerSearch'; |
6 | 6 | import { ControlGroup } from '../ControlGroup'; |
7 | 7 | |
8 | 8 | export enum FormFieldsEnum { |
9 | - ADDRESS = 'address', | |
10 | - FUNCTION_CODE = 'functionCode', | |
11 | - START_REGISTER_ADDRESS = 'startRegisterAddress', | |
12 | - DATA_VALID = 'dataValid', | |
9 | + // 设备地址码 | |
10 | + DEVICE_CODE = 'deviceCode', | |
11 | + // 功能码 | |
12 | + METHOD = 'method', | |
13 | + // 寄存器地址 | |
14 | + REGISTER_ADDR = 'registerAddr', | |
15 | + // 数据校验算法 | |
16 | + CRC = 'crc', | |
13 | 17 | // 线圈个数 |
14 | 18 | COIL_NUMBER = 'coilNumber', |
15 | 19 | // 寄存器个数 |
... | ... | @@ -27,7 +31,7 @@ export enum FormFieldsEnum { |
27 | 31 | useComponentRegister('RegisterAddressInput', RegisterAddressInput); |
28 | 32 | useComponentRegister('ControlGroup', ControlGroup); |
29 | 33 | |
30 | -enum FunctionCodeEnum { | |
34 | +export enum FunctionCodeEnum { | |
31 | 35 | // 读取线圈状态01 |
32 | 36 | READ_COIL_STATE_01 = '01', |
33 | 37 | // 读取输入状态02 |
... | ... | @@ -46,35 +50,45 @@ enum FunctionCodeEnum { |
46 | 50 | WRITE_MULTIPLE_KEEP_REGISTER_16 = '16', |
47 | 51 | } |
48 | 52 | |
49 | -const showCoilNumber = (value: FunctionCodeEnum) => | |
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) => | |
50 | 64 | [ |
51 | 65 | FunctionCodeEnum.READ_COIL_STATE_01, |
52 | 66 | FunctionCodeEnum.READ_INPUT_STATE_02, |
53 | 67 | FunctionCodeEnum.WRITE_MULTIPLE_COIL_STATE_15, |
54 | 68 | ].includes(value); |
55 | 69 | |
56 | -const showRegisterNumber = (value: FunctionCodeEnum) => | |
70 | +export const showRegisterNumber = (value: FunctionCodeEnum) => | |
57 | 71 | [ |
58 | 72 | FunctionCodeEnum.READ_KEEP_REGISTER_03, |
59 | 73 | FunctionCodeEnum.READ_INPUT_REGISTER_04, |
60 | 74 | FunctionCodeEnum.WRITE_MULTIPLE_KEEP_REGISTER_16, |
61 | 75 | ].includes(value); |
62 | 76 | |
63 | -const showCoilValue = (value: FunctionCodeEnum) => | |
77 | +export const showCoilValue = (value: FunctionCodeEnum) => | |
64 | 78 | [FunctionCodeEnum.WRITE_SINGLE_COIL_REGISTER_05].includes(value); |
65 | 79 | |
66 | -const showRegisterValue = (value: FunctionCodeEnum) => | |
80 | +export const showRegisterValue = (value: FunctionCodeEnum) => | |
67 | 81 | [FunctionCodeEnum.WRITE_SINGLE_KEEP_COIL_REGISTER_06].includes(value); |
68 | 82 | |
69 | -const isWriteCoilGroup = (value: FunctionCodeEnum) => | |
83 | +export const isWriteCoilGroup = (value: FunctionCodeEnum) => | |
70 | 84 | [FunctionCodeEnum.WRITE_MULTIPLE_COIL_STATE_15].includes(value); |
71 | 85 | |
72 | -const isWriteRegisterGroup = (value: FunctionCodeEnum) => | |
86 | +export const isWriteRegisterGroup = (value: FunctionCodeEnum) => | |
73 | 87 | [FunctionCodeEnum.WRITE_MULTIPLE_KEEP_REGISTER_16].includes(value); |
74 | 88 | |
75 | 89 | export const formSchemas: FormSchema[] = [ |
76 | 90 | { |
77 | - field: FormFieldsEnum.ADDRESS, | |
91 | + field: FormFieldsEnum.DEVICE_CODE, | |
78 | 92 | component: 'ApiSelect', |
79 | 93 | label: '从机地址', |
80 | 94 | rules: [{ required: true, message: '请选择从机地址' }], |
... | ... | @@ -103,7 +117,7 @@ export const formSchemas: FormSchema[] = [ |
103 | 117 | }, |
104 | 118 | }, |
105 | 119 | { |
106 | - field: FormFieldsEnum.FUNCTION_CODE, | |
120 | + field: FormFieldsEnum.METHOD, | |
107 | 121 | component: 'ApiSelect', |
108 | 122 | label: '功能码', |
109 | 123 | defaultValue: '01', |
... | ... | @@ -121,18 +135,18 @@ export const formSchemas: FormSchema[] = [ |
121 | 135 | }, |
122 | 136 | }, |
123 | 137 | { |
124 | - field: FormFieldsEnum.START_REGISTER_ADDRESS, | |
138 | + field: FormFieldsEnum.REGISTER_ADDR, | |
125 | 139 | label: '起始寄存器地址', |
126 | 140 | component: 'RegisterAddressInput', |
127 | 141 | valueField: 'value', |
128 | 142 | changeEvent: 'update:value', |
129 | - defaultValue: '0', | |
143 | + defaultValue: 0, | |
130 | 144 | }, |
131 | 145 | { |
132 | 146 | field: FormFieldsEnum.REGISTER_NUMBER, |
133 | 147 | label: '寄存器个数', |
134 | 148 | component: 'InputNumber', |
135 | - ifShow: ({ model }) => showRegisterNumber(model[FormFieldsEnum.FUNCTION_CODE]), | |
149 | + ifShow: ({ model }) => showRegisterNumber(model[FormFieldsEnum.METHOD]), | |
136 | 150 | defaultValue: 1, |
137 | 151 | rules: [{ required: true, message: '请输入寄存器个数' }], |
138 | 152 | componentProps: { |
... | ... | @@ -146,7 +160,7 @@ export const formSchemas: FormSchema[] = [ |
146 | 160 | field: FormFieldsEnum.COIL_NUMBER, |
147 | 161 | label: '线圈个数', |
148 | 162 | component: 'InputNumber', |
149 | - ifShow: ({ model }) => showCoilNumber(model[FormFieldsEnum.FUNCTION_CODE]), | |
163 | + ifShow: ({ model }) => showCoilNumber(model[FormFieldsEnum.METHOD]), | |
150 | 164 | defaultValue: 1, |
151 | 165 | rules: [{ required: true, message: '请输入线圈个数' }], |
152 | 166 | componentProps: { |
... | ... | @@ -162,7 +176,7 @@ export const formSchemas: FormSchema[] = [ |
162 | 176 | component: 'RegisterAddressInput', |
163 | 177 | valueField: 'value', |
164 | 178 | changeEvent: 'update:value', |
165 | - ifShow: ({ model }) => showCoilValue(model[FormFieldsEnum.FUNCTION_CODE]), | |
179 | + ifShow: ({ model }) => showCoilValue(model[FormFieldsEnum.METHOD]), | |
166 | 180 | defaultValue: '0', |
167 | 181 | rules: [{ required: true, message: '请输入线圈值' }], |
168 | 182 | componentProps: { |
... | ... | @@ -175,7 +189,7 @@ export const formSchemas: FormSchema[] = [ |
175 | 189 | component: 'RegisterAddressInput', |
176 | 190 | valueField: 'value', |
177 | 191 | changeEvent: 'update:value', |
178 | - ifShow: ({ model }) => showRegisterValue(model[FormFieldsEnum.FUNCTION_CODE]), | |
192 | + ifShow: ({ model }) => showRegisterValue(model[FormFieldsEnum.METHOD]), | |
179 | 193 | defaultValue: '0', |
180 | 194 | rules: [{ required: true, message: '请输入寄存器值' }], |
181 | 195 | componentProps: { |
... | ... | @@ -189,7 +203,7 @@ export const formSchemas: FormSchema[] = [ |
189 | 203 | colProps: { span: 24 }, |
190 | 204 | valueField: 'value', |
191 | 205 | changeEvent: 'update:value', |
192 | - ifShow: ({ model }) => isWriteRegisterGroup(model[FormFieldsEnum.FUNCTION_CODE]), | |
206 | + ifShow: ({ model }) => isWriteRegisterGroup(model[FormFieldsEnum.METHOD]), | |
193 | 207 | componentProps: ({ formModel }) => { |
194 | 208 | const length = formModel[FormFieldsEnum.REGISTER_NUMBER]; |
195 | 209 | return { |
... | ... | @@ -213,7 +227,7 @@ export const formSchemas: FormSchema[] = [ |
213 | 227 | colProps: { span: 24 }, |
214 | 228 | valueField: 'value', |
215 | 229 | changeEvent: 'update:value', |
216 | - ifShow: ({ model }) => isWriteCoilGroup(model[FormFieldsEnum.FUNCTION_CODE]), | |
230 | + ifShow: ({ model }) => isWriteCoilGroup(model[FormFieldsEnum.METHOD]), | |
217 | 231 | componentProps: ({ formModel }) => { |
218 | 232 | const length = formModel[FormFieldsEnum.COIL_NUMBER]; |
219 | 233 | return { |
... | ... | @@ -235,11 +249,12 @@ export const formSchemas: FormSchema[] = [ |
235 | 249 | }, |
236 | 250 | }, |
237 | 251 | { |
238 | - field: FormFieldsEnum.DATA_VALID, | |
252 | + field: FormFieldsEnum.CRC, | |
239 | 253 | label: '数据校验', |
240 | 254 | component: 'ApiSelect', |
241 | 255 | colProps: { span: 24 }, |
242 | 256 | rules: [{ required: true, message: '请选择数据校验方式' }], |
257 | + defaultValue: CRCValidTypeEnum.CRC_16_LOWER, | |
243 | 258 | componentProps: () => { |
244 | 259 | return { |
245 | 260 | api: findDictItemByCode, | ... | ... |
... | ... | @@ -4,11 +4,10 @@ |
4 | 4 | import { SettingOutlined } from '@ant-design/icons-vue'; |
5 | 5 | import { ModeEnum } from './index'; |
6 | 6 | import { computed } from '@vue/reactivity'; |
7 | - import CreateTCPCommandModal from './CreateTCPCommandModal.vue'; | |
7 | + import GenModbusCommandModal from './GenModbusCommandModal.vue'; | |
8 | 8 | import { useModal } from '/@/components/Modal'; |
9 | 9 | import { ref } from 'vue'; |
10 | 10 | import { unref } from 'vue'; |
11 | - import JSONEditorType from 'jsoneditor'; | |
12 | 11 | |
13 | 12 | const props = withDefaults( |
14 | 13 | defineProps<{ |
... | ... | @@ -54,8 +53,7 @@ |
54 | 53 | } |
55 | 54 | }; |
56 | 55 | |
57 | - const handleEditorBlur = (_event: Event, instance: JSONEditorType) => { | |
58 | - const value = instance.getText(); | |
56 | + const handleUpdateEditorValue = (value: string) => { | |
59 | 57 | handleEmit(value); |
60 | 58 | }; |
61 | 59 | </script> |
... | ... | @@ -74,7 +72,11 @@ |
74 | 72 | <SettingOutlined class="cursor-pointer" @click="handleClick" /> |
75 | 73 | </template> |
76 | 74 | </Input> |
77 | - <JSONEditor v-if="mode === ModeEnum.JSON" :value="getJSONValue" @blur="handleEditorBlur" /> | |
78 | - <CreateTCPCommandModal @register="registerCreateTCPCommandModal" @update:value="handleEmit" /> | |
75 | + <JSONEditor | |
76 | + v-if="mode === ModeEnum.JSON" | |
77 | + :value="getJSONValue" | |
78 | + @update:value="handleUpdateEditorValue" | |
79 | + /> | |
80 | + <GenModbusCommandModal @register="registerCreateTCPCommandModal" @update:value="handleEmit" /> | |
79 | 81 | </section> |
80 | 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 | +}; | ... | ... |
... | ... | @@ -14,6 +14,8 @@ |
14 | 14 | import { computed } from '@vue/reactivity'; |
15 | 15 | import { unref } from 'vue'; |
16 | 16 | import { ref } from 'vue'; |
17 | + import { dateUtil } from '/@/utils/dateUtil'; | |
18 | + import { DEFAULT_DATE_FORMAT } from '/@/views/visual/board/detail/config/util'; | |
17 | 19 | |
18 | 20 | enum DropMenuEvent { |
19 | 21 | DELETE = 'DELETE', |
... | ... | @@ -43,9 +45,9 @@ |
43 | 45 | }); |
44 | 46 | |
45 | 47 | const getCancelState = computed(() => { |
46 | - const { executeTarget } = unref(getRecord); | |
47 | - const { cancelExecuteDevices } = executeTarget; | |
48 | - return !!(cancelExecuteDevices && cancelExecuteDevices.length); | |
48 | + return !!( | |
49 | + unref(getRecord).tkDeviceTaskCenter && unref(getRecord).tkDeviceTaskCenter?.allowState | |
50 | + ); | |
49 | 51 | }); |
50 | 52 | |
51 | 53 | const handleDelete = async () => { |
... | ... | @@ -76,7 +78,11 @@ |
76 | 78 | try { |
77 | 79 | if (!props.tbDeviceId) return; |
78 | 80 | loading.value = true; |
79 | - await cancelTask({ id: unref(getRecord).id, tbDeviceId: props.tbDeviceId }); | |
81 | + await cancelTask({ | |
82 | + id: unref(getRecord).id, | |
83 | + tbDeviceId: props.tbDeviceId, | |
84 | + allow: !unref(getCancelState), | |
85 | + }); | |
80 | 86 | createMessage.success('设置成功'); |
81 | 87 | props.reload?.(); |
82 | 88 | } catch (error) { |
... | ... | @@ -85,6 +91,31 @@ |
85 | 91 | loading.value = false; |
86 | 92 | } |
87 | 93 | }; |
94 | + | |
95 | + const getTwoDateDiff = (date: number, now = dateUtil()) => { | |
96 | + const unitList = [ | |
97 | + { radix: null, unitName: '年', unit: 'year' }, | |
98 | + { radix: 11, unitName: '月', unit: 'month' }, | |
99 | + { radix: 30, unitName: '日', unit: 'day' }, | |
100 | + { radix: 23, unitName: '时', unit: 'hour' }, | |
101 | + { radix: 59, unitName: '分', unit: 'minute' }, | |
102 | + { radix: 59, unitName: '秒', unit: 'second' }, | |
103 | + ]; | |
104 | + | |
105 | + for (let i = unitList.length - 1; i >= 0; i--) { | |
106 | + const { unit, radix, unitName } = unitList[i]; | |
107 | + const lastDate = dateUtil(date); | |
108 | + const diffValue = now.diff(lastDate, unit as any); | |
109 | + if (!radix || diffValue <= radix) { | |
110 | + return { value: diffValue, unit, unitName }; | |
111 | + } | |
112 | + } | |
113 | + }; | |
114 | + | |
115 | + const getLastExecuteTime = computed(() => { | |
116 | + if (!unref(getRecord).lastExecuteTime) return; | |
117 | + return getTwoDateDiff(unref(getRecord).lastExecuteTime!); | |
118 | + }); | |
88 | 119 | </script> |
89 | 120 | |
90 | 121 | <template> |
... | ... | @@ -155,10 +186,15 @@ |
155 | 186 | <span>运行任务</span> |
156 | 187 | </div> |
157 | 188 | </Button> |
158 | - <Tooltip title="最后运行时间:"> | |
189 | + <Tooltip | |
190 | + v-if="getLastExecuteTime" | |
191 | + overlay-class-name="task-last-execute-time-tooltip" | |
192 | + placement="topLeft" | |
193 | + :title="`最后运行时间: ${dateUtil(getRecord.lastExecuteTime).format(DEFAULT_DATE_FORMAT)}`" | |
194 | + > | |
159 | 195 | <div class="text-gray-400 text-xs truncate"> |
160 | 196 | <span class="mr-2">间隔时间重复</span> |
161 | - <span>三分钟前</span> | |
197 | + <span>{{ `${getLastExecuteTime.value}${getLastExecuteTime.unitName}前` }}</span> | |
162 | 198 | </div> |
163 | 199 | </Tooltip> |
164 | 200 | </div> |
... | ... | @@ -184,3 +220,9 @@ |
184 | 220 | </section> |
185 | 221 | </Card> |
186 | 222 | </template> |
223 | + | |
224 | +<style> | |
225 | + .task-last-execute-time-tooltip { | |
226 | + max-width: 300px; | |
227 | + } | |
228 | +</style> | ... | ... |
... | ... | @@ -52,19 +52,6 @@ export const formSchemas: FormSchema[] = [ |
52 | 52 | getPopupContainer: () => document.body, |
53 | 53 | }, |
54 | 54 | }, |
55 | - // { | |
56 | - // label: '', | |
57 | - // component: 'Select', | |
58 | - // field: FormFieldsEnum.TASK_TYPE, | |
59 | - // defaultValue: TaskTypeEnum.ALL, | |
60 | - // componentProps: { | |
61 | - // options: [ | |
62 | - // { label: TaskTypeNameEnum.CUSTOM, value: TaskTypeEnum.CUSTOM }, | |
63 | - // { label: TaskTypeNameEnum.MODBUS_RTU, value: TaskTypeEnum.MODBUS_RTU }, | |
64 | - // ], | |
65 | - // getPopupContainer: () => document.body, | |
66 | - // }, | |
67 | - // }, | |
68 | 55 | { |
69 | 56 | label: '状态', |
70 | 57 | component: 'Select', | ... | ... |
... | ... | @@ -5,7 +5,7 @@ |
5 | 5 | import { formSchemas } from './config'; |
6 | 6 | import { TaskCard } from './components/TaskCard'; |
7 | 7 | import { getTaskCenterList } from '/@/api/task'; |
8 | - import { ref } from 'vue'; | |
8 | + import { ref, unref } from 'vue'; | |
9 | 9 | import { onMounted } from 'vue'; |
10 | 10 | import { DetailModal } from './components/DetailModal'; |
11 | 11 | import { useModal } from '/@/components/Modal'; |
... | ... | @@ -14,6 +14,8 @@ |
14 | 14 | import { ReloadOutlined } from '@ant-design/icons-vue'; |
15 | 15 | import { DataActionModeEnum } from '/@/enums/toolEnum'; |
16 | 16 | import { ModalParamsType } from '/#/utils'; |
17 | + import { Authority } from '/@/components/Authority'; | |
18 | + import { getBoundingClientRect } from '/@/utils/domUtils'; | |
17 | 19 | |
18 | 20 | const [registerModal, { openModal }] = useModal(); |
19 | 21 | |
... | ... | @@ -61,7 +63,28 @@ |
61 | 63 | |
62 | 64 | const reload = () => getDataSource(); |
63 | 65 | |
66 | + const listElRef = ref<Nullable<ComponentElRef>>(null); | |
67 | + | |
68 | + const setListHeight = () => { | |
69 | + const clientHeight = document.documentElement.clientHeight; | |
70 | + const rect = getBoundingClientRect(unref(listElRef)!.$el!) as DOMRect; | |
71 | + // margin-top 24 height 24 | |
72 | + const paginationHeight = 24 + 24 + 8; | |
73 | + // list pading top 8 maring-top 8 extra slot 56 | |
74 | + const listContainerMarginBottom = 8 + 8 + 56; | |
75 | + const listContainerHeight = | |
76 | + clientHeight - rect.top - paginationHeight - listContainerMarginBottom; | |
77 | + const listContainerEl = (unref(listElRef)!.$el as HTMLElement).querySelector( | |
78 | + '.ant-spin-container' | |
79 | + ) as HTMLElement; | |
80 | + listContainerEl && | |
81 | + (listContainerEl.style.height = listContainerHeight + 'px') && | |
82 | + (listContainerEl.style.overflowY = 'auto') && | |
83 | + (listContainerEl.style.overflowX = 'hidden'); | |
84 | + }; | |
85 | + | |
64 | 86 | onMounted(() => { |
87 | + setListHeight(); | |
65 | 88 | getDataSource(); |
66 | 89 | }); |
67 | 90 | </script> |
... | ... | @@ -72,11 +95,13 @@ |
72 | 95 | class="bg-light-50 flex p-4 justify-between items-center x dark:text-gray-300 dark:bg-dark-900" |
73 | 96 | > |
74 | 97 | <div class="text-2xl">任务</div> |
75 | - <Button | |
76 | - type="primary" | |
77 | - @click="openModal(true, { mode: DataActionModeEnum.CREATE } as ModalParamsType)" | |
78 | - >创建任务</Button | |
79 | - > | |
98 | + <Authority> | |
99 | + <Button | |
100 | + type="primary" | |
101 | + @click="openModal(true, { mode: DataActionModeEnum.CREATE } as ModalParamsType)" | |
102 | + >创建任务 | |
103 | + </Button> | |
104 | + </Authority> | |
80 | 105 | </section> |
81 | 106 | <section |
82 | 107 | class="form-container bg-light-50 px-4 pt-4 mt-4 x dark:text-gray-300 dark:bg-dark-900" |
... | ... | @@ -85,6 +110,7 @@ |
85 | 110 | </section> |
86 | 111 | <section class="bg-light-50 mt-4 p-4 x dark:text-gray-300 dark:bg-dark-900"> |
87 | 112 | <List |
113 | + ref="listElRef" | |
88 | 114 | :dataSource="dataSource" |
89 | 115 | :pagination="pagination" |
90 | 116 | :grid="{ gutter: 16, xs: 1, sm: 1, md: 2, lg: 3, xl: 3, xxl: 4, column: 4 }" |
... | ... | @@ -92,7 +118,6 @@ |
92 | 118 | > |
93 | 119 | <template #header> |
94 | 120 | <section class="flex justify-end gap-4"> |
95 | - <!-- <CardLayoutButton /> --> | |
96 | 121 | <Tooltip title="刷新"> |
97 | 122 | <Button type="primary" @click="getDataSource"> |
98 | 123 | <ReloadOutlined :spin="loading" /> | ... | ... |