Commit cbb36837759d57efcd0c5004afb3044819d3ab53

Authored by xp.Huang
2 parents c9cd2fad 3d1616dc

Merge remote-tracking branch 'origin/ww'

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