Commit 1ec598961e777dbfe5c201ded30aa680e4810ccd

Authored by ww
1 parent f17f2ee0

feat: 任务中心基本功能开发

Showing 34 changed files with 2043 additions and 1 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 { CreateTaskRecordType, GetTaskListParamsType, TaskRecordType } from './model';
  2 +import { PaginationResult } from '/#/axios';
  3 +import { defHttp } from '/@/utils/http/axios';
  4 +
  5 +enum Api {
  6 + TASK_LIST = '/task_center',
  7 + ADD_TASK = '/task_center/add',
  8 + UPDATE_STATE = '/task_center',
  9 + DELETE_TASK = '/task_center',
  10 + UPDATE_TASK = '/task_center/update',
  11 + CANCEL_TASK = '/task_center',
  12 +}
  13 +
  14 +export const getTaskCenterList = (params: GetTaskListParamsType) => {
  15 + return defHttp.get<PaginationResult<TaskRecordType>>({
  16 + url: Api.TASK_LIST,
  17 + params,
  18 + });
  19 +};
  20 +
  21 +export const createTask = (data: CreateTaskRecordType) => {
  22 + return defHttp.post({
  23 + url: Api.ADD_TASK,
  24 + data,
  25 + });
  26 +};
  27 +
  28 +export const updateState = (id: string, state: number) => {
  29 + return defHttp.put({
  30 + url: `${Api.UPDATE_STATE}/${id}/update/${state}`,
  31 + });
  32 +};
  33 +
  34 +export const deleteTask = (ids: string[]) => {
  35 + return defHttp.delete({
  36 + url: Api.DELETE_TASK,
  37 + data: { ids },
  38 + });
  39 +};
  40 +
  41 +export const updateTask = (data: CreateTaskRecordType & Record<'id', string>) => {
  42 + return defHttp.put({
  43 + url: Api.UPDATE_TASK,
  44 + data,
  45 + });
  46 +};
  47 +
  48 +export const cancelTask = (data: Record<'id' | 'tbDeviceId', string>) => {
  49 + return defHttp.put({
  50 + url: `${Api.CANCEL_TASK}/${data.id}/cancel/${data.tbDeviceId}`,
  51 + });
  52 +};
... ...
  1 +import { TaskTargetEnum } from '/@/views/task/center/config';
  2 +import {
  3 + ExecuteTimeTypeEnum,
  4 + PeriodTypeEnum,
  5 + PushWayEnum,
  6 +} from '/@/views/task/center/components/CreateModal/config';
  7 +import { TaskTypeEnum } from '/@/views/task/center/config';
  8 +
  9 +export interface GetTaskListParamsType {
  10 + page: number;
  11 + pageSize: number;
  12 + state?: string;
  13 +}
  14 +
  15 +export interface CreateTaskRecordType {
  16 + name: string;
  17 + targetType: TaskTargetEnum;
  18 + executeTarget: {
  19 + organizationId?: string;
  20 + deviceProfileId?: string;
  21 + deviceType?: string;
  22 + cancelExecuteDevices?: string[];
  23 + data?: string[];
  24 + };
  25 + executeContent: {
  26 + pushContent: {
  27 + rpcCommand: string | Recordable;
  28 + };
  29 + pushWay: PushWayEnum;
  30 + type: TaskTypeEnum;
  31 + };
  32 + executeTime: {
  33 + cron: string;
  34 + type: ExecuteTimeTypeEnum;
  35 + periodType: PeriodTypeEnum;
  36 + period: string;
  37 + time: string;
  38 + pollUnit: string;
  39 + };
  40 +}
  41 +
  42 +export interface TaskRecordType extends CreateTaskRecordType {
  43 + id: string;
  44 + createTime: string;
  45 + creator: string;
  46 + enabled: boolean;
  47 + state: number;
  48 +}
... ...
... ... @@ -2,6 +2,7 @@ import { withInstall } from '/@/utils';
2 2 // @ts-ignore
3 3 import codeEditor from './src/CodeEditor.vue';
4 4 import jsonPreview from './src/json-preview/JsonPreview.vue';
  5 +export { JSONEditor } from './src/JSONEditor';
5 6
6 7 export const CodeEditor = withInstall(codeEditor);
7 8 export const JsonPreview = withInstall(jsonPreview);
... ...
  1 +import { Rule } from '/@/components/Form';
  2 +
  3 +export { default as JSONEditor } from './index.vue';
  4 +
  5 +export const parseStringToJSON = <T = Recordable>(value: string) => {
  6 + try {
  7 + const json = JSON.parse(value) as T;
  8 + return { json, valid: true };
  9 + } catch (error) {
  10 + return { json: null, valid: false };
  11 + }
  12 +};
  13 +
  14 +export const JSONEditorValidator = (message = 'json格式校验失败'): Rule[] => {
  15 + return [
  16 + {
  17 + validateTrigger: 'blur',
  18 + validator(_rule: Rule, value: any, _callback: Fn) {
  19 + const { valid } = parseStringToJSON(value);
  20 + if (valid) {
  21 + return Promise.resolve();
  22 + }
  23 + return Promise.reject(message);
  24 + },
  25 + },
  26 + ];
  27 +};
... ...
  1 +<script lang="ts" setup>
  2 + import { ref } from 'vue';
  3 + import JSONEditor, { JSONEditorOptions } from 'jsoneditor';
  4 + import 'jsoneditor/dist/jsoneditor.min.css';
  5 + import { unref } from 'vue';
  6 + import { onMounted } from 'vue';
  7 + import { computed } from '@vue/reactivity';
  8 + import { onUnmounted } from 'vue';
  9 + import { watch } from 'vue';
  10 +
  11 + enum EventEnum {
  12 + UPDATE_VALUE = 'update:value',
  13 + CHANGE = 'change',
  14 + BLUR = 'blur',
  15 + FOCUS = 'focus',
  16 + }
  17 +
  18 + const props = withDefaults(
  19 + defineProps<{
  20 + value?: string;
  21 + options?: JSONEditorOptions;
  22 + }>(),
  23 + {
  24 + options: () =>
  25 + ({
  26 + mode: 'code',
  27 + mainMenuBar: false,
  28 + statusBar: false,
  29 + } as JSONEditorOptions),
  30 + }
  31 + );
  32 +
  33 + const emit = defineEmits<{
  34 + (e: EventEnum.UPDATE_VALUE, value: any, instance?: JSONEditor): void;
  35 + (e: EventEnum.CHANGE, value: any, instance?: JSONEditor): void;
  36 + (e: EventEnum.BLUR, event: Event, instance?: JSONEditor): void;
  37 + (e: EventEnum.FOCUS, event: Event, instance?: JSONEditor): void;
  38 + }>();
  39 +
  40 + const jsonEditorElRef = ref<Nullable<any>>();
  41 +
  42 + const editoreRef = ref<JSONEditor>();
  43 +
  44 + const handleChange = (value: any) => {
  45 + emit(EventEnum.UPDATE_VALUE, value, unref(editoreRef));
  46 + emit(EventEnum.CHANGE, value, unref(editoreRef));
  47 + };
  48 +
  49 + const handleEmit = (event: Event, key: EventEnum) => {
  50 + emit(key as EventEnum[keyof EventEnum], event, unref(editoreRef));
  51 + };
  52 +
  53 + const getOptions = computed(() => {
  54 + const { options } = props;
  55 + return {
  56 + ...options,
  57 + onChangeText: handleChange,
  58 + onBlur: (event: Event) => handleEmit(event, EventEnum.BLUR),
  59 + onFocus: (event: Event) => handleEmit(event, EventEnum.FOCUS),
  60 + } as JSONEditorOptions;
  61 + });
  62 +
  63 + const initialize = () => {
  64 + editoreRef.value = new JSONEditor(unref(jsonEditorElRef), unref(getOptions));
  65 + };
  66 +
  67 + watch(
  68 + () => props.value,
  69 + (target) => {
  70 + unref(editoreRef)?.setText(target || '');
  71 + },
  72 + {
  73 + immediate: true,
  74 + }
  75 + );
  76 +
  77 + const get = (): string => {
  78 + return unref(editoreRef)?.getText() || '';
  79 + };
  80 +
  81 + const set = (data: any) => {
  82 + return unref(editoreRef)?.set(data);
  83 + };
  84 +
  85 + onMounted(() => {
  86 + initialize();
  87 + });
  88 +
  89 + onUnmounted(() => {
  90 + unref(editoreRef)?.destroy();
  91 + });
  92 +
  93 + defineExpose({
  94 + get,
  95 + set,
  96 + });
  97 +</script>
  98 +
  99 +<template>
  100 + <div class="p-2 bg-gray-200">
  101 + <div ref="jsonEditorElRef" class="jsoneditor"></div>
  102 + </div>
  103 +</template>
  104 +
  105 +<style lang="less" scoped>
  106 + .jsoneditor {
  107 + border: none !important;
  108 +
  109 + :deep(.jsoneditor) {
  110 + border: none !important;
  111 + }
  112 + }
  113 +</style>
... ...
... ... @@ -122,4 +122,8 @@ export type ComponentType =
122 122 | 'ApiSelectScrollLoad'
123 123 | 'TransferModal'
124 124 | 'TransferTableModal'
125   - | 'ObjectModelValidateForm';
  125 + | 'ObjectModelValidateForm'
  126 + | 'DevicePicker'
  127 + | 'CustomInput'
  128 + | 'RegisterAddressInput'
  129 + | 'ControlGroup';
... ...
  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 + defineProps<{
  15 + tbDeviceId: string;
  16 + }>();
  17 +
  18 + const listElRef = ref<Nullable<ComponentElRef>>(null);
  19 +
  20 + const [registerForm, { getFieldsValue }] = useForm({
  21 + schemas: formSchemas,
  22 + baseColProps: { span: 8 },
  23 + compact: true,
  24 + showAdvancedButton: true,
  25 + labelWidth: 100,
  26 + submitFunc: async () => {
  27 + pagination.params = getFieldsValue();
  28 + getDataSource();
  29 + },
  30 + });
  31 +
  32 + const pagination = reactive({
  33 + total: 10,
  34 + current: 1,
  35 + pageSize: 10,
  36 + showQuickJumper: true,
  37 + size: 'small',
  38 + showTotal: (total: number) => `共 ${total} 条数据`,
  39 + params: {} as Recordable,
  40 + });
  41 +
  42 + const dataSource = ref<TaskRecordType[]>([]);
  43 + const loading = ref(false);
  44 +
  45 + const getDataSource = async () => {
  46 + try {
  47 + loading.value = true;
  48 + const { items } = await getTaskCenterList({
  49 + page: pagination.current,
  50 + pageSize: pagination.pageSize,
  51 + ...pagination.params,
  52 + });
  53 + dataSource.value = items;
  54 + } catch (error) {
  55 + throw error;
  56 + } finally {
  57 + loading.value = false;
  58 + }
  59 + };
  60 +
  61 + const reload = () => getDataSource();
  62 +
  63 + const setListHeight = () => {
  64 + const clientHeight = document.documentElement.clientHeight;
  65 + const rect = getBoundingClientRect(unref(listElRef)!.$el!) as DOMRect;
  66 + // margin-top 24 height 24
  67 + const paginationHeight = 24 + 24 + 8;
  68 + // list pading top 8 maring-top 8 extra slot 56
  69 + const listContainerMarginBottom = 8 + 8 + 56;
  70 + const listContainerHeight =
  71 + clientHeight - rect.top - paginationHeight - listContainerMarginBottom;
  72 + const listContainerEl = (unref(listElRef)!.$el as HTMLElement).querySelector(
  73 + '.ant-spin-container'
  74 + ) as HTMLElement;
  75 + listContainerEl &&
  76 + (listContainerEl.style.height = listContainerHeight + 'px') &&
  77 + (listContainerEl.style.overflowY = 'auto') &&
  78 + (listContainerEl.style.overflowX = 'hidden');
  79 + };
  80 +
  81 + onMounted(() => {
  82 + setListHeight();
  83 + getDataSource();
  84 + });
  85 +</script>
  86 +
  87 +<template>
  88 + <PageWrapper class="bg-gray-100">
  89 + <section
  90 + class="form-container bg-light-50 px-4 pt-4 mt-4 x dark:text-gray-300 dark:bg-dark-900"
  91 + >
  92 + <BasicForm @register="registerForm" />
  93 + </section>
  94 + <section class="bg-light-50 my-4 p-4 x dark:text-gray-300 dark:bg-dark-900">
  95 + <List
  96 + ref="listElRef"
  97 + :dataSource="dataSource"
  98 + :pagination="pagination"
  99 + :grid="{ gutter: 16, xs: 1, sm: 1, md: 1, lg: 2, xl: 2, xxl: 3, column: 3 }"
  100 + :loading="loading"
  101 + >
  102 + <template #header>
  103 + <section class="flex justify-end gap-4">
  104 + <Tooltip title="刷新">
  105 + <Button type="primary" @click="getDataSource">
  106 + <ReloadOutlined :spin="loading" />
  107 + </Button>
  108 + </Tooltip>
  109 + </section>
  110 + </template>
  111 + <template #renderItem="{ item }">
  112 + <List.Item :key="item.id">
  113 + <TaskCard
  114 + :record="item"
  115 + :reload="reload"
  116 + :tbDeviceId="tbDeviceId"
  117 + :deviceTaskCardMode="true"
  118 + />
  119 + </List.Item>
  120 + </template>
  121 + </List>
  122 + </section>
  123 + </PageWrapper>
  124 +</template>
... ...
  1 +import { Component } from 'vue';
  2 +import { ComponentType } from './type';
  3 +
  4 +const componentMap = new Map<ComponentType, Component>();
  5 +
  6 +export function add(compName: ComponentType, component: Component) {
  7 + componentMap.set(compName, component);
  8 +}
  9 +
  10 +export function del(compName: ComponentType) {
  11 + componentMap.delete(compName);
  12 +}
  13 +
  14 +export { componentMap };
... ...
  1 +import type { ComponentType } from './type';
  2 +import { tryOnUnmounted } from '@vueuse/core';
  3 +import { add, del } from './componentMap';
  4 +import type { Component } from 'vue';
  5 +
  6 +export function useComponentRegister(compName: ComponentType, comp: Component) {
  7 + add(compName, comp);
  8 + tryOnUnmounted(() => {
  9 + del(compName);
  10 + });
  11 +}
... ...
  1 +export { default as ControlGroup } from './index.vue';
  2 +export { add, del } from './componentMap';
  3 +export { useComponentRegister } from './help';
... ...
  1 +<script lang="ts" setup>
  2 + import { BasicForm, FormSchema, useForm } from '/@/components/Form';
  3 + import { ComponentType, ColEx } from '/@/components/Form/src/types/index';
  4 +
  5 + withDefaults(
  6 + defineProps<{
  7 + component?: ComponentType;
  8 + itemColProps?: Partial<ColEx>;
  9 + }>(),
  10 + {
  11 + component: 'Switch',
  12 + itemColProps: () => ({ span: 12 } as Partial<ColEx>),
  13 + }
  14 + );
  15 +
  16 + const [registerForm, {}] = useForm({
  17 + showActionButtonGroup: false,
  18 + schemas: Array.from({ length: 3 }).map((_item, index) => {
  19 + return {
  20 + field: index.toString(),
  21 + label: index.toString(),
  22 + component: 'Switch',
  23 + } as FormSchema;
  24 + }),
  25 + // baseColProps,
  26 + });
  27 +</script>
  28 +
  29 +<template>
  30 + <BasicForm @register="registerForm" />
  31 +</template>
... ...
  1 +export type ComponentType = '';
... ...
  1 +<script lang="ts" setup>
  2 + import { Button } from 'ant-design-vue';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import { BasicModal, useModal } from '/@/components/Modal';
  5 + import { formSchemas } from './config';
  6 + import { ref } from 'vue';
  7 + import { unref } from 'vue';
  8 +
  9 + const emit = defineEmits(['update:value']);
  10 +
  11 + const [registerModal] = useModal();
  12 +
  13 + const [registerForm, {}] = useForm({
  14 + schemas: formSchemas,
  15 + showActionButtonGroup: false,
  16 + rowProps: { gutter: 10 },
  17 + baseColProps: { span: 12 },
  18 + });
  19 +
  20 + const commandValue = ref('');
  21 +
  22 + const handleGetValue = async () => {};
  23 +
  24 + const handleOk = () => {
  25 + emit('update:value', unref(commandValue));
  26 + };
  27 +</script>
  28 +
  29 +<template>
  30 + <BasicModal @register="registerModal" title="配置操作" @ok="handleOk">
  31 + <BasicForm @register="registerForm" class="create-tcp-command-form" />
  32 + <section>
  33 + <Button @click="handleGetValue" type="link" class="!px-0">生成预览</Button>
  34 + <div v-if="commandValue">
  35 + <div class="text-gray-400">Modbus 指令预览</div>
  36 + <code class="bg-dark-50 text-light-50 p-1 w-full block mt-1">{{ commandValue }}</code>
  37 + </div>
  38 + </section>
  39 + </BasicModal>
  40 +</template>
  41 +
  42 +<style lang="less" scoped>
  43 + .create-tcp-command-form {
  44 + :deep(.ant-input-number) {
  45 + width: 100%;
  46 + min-width: 0;
  47 + }
  48 + }
  49 +</style>
... ...
  1 +<script lang="ts" setup>
  2 + import { InputGroup, InputNumber, Select, Input } from 'ant-design-vue';
  3 + import { watch } from 'vue';
  4 + import { unref } from 'vue';
  5 + import { computed } from 'vue';
  6 + import { ref } from 'vue';
  7 +
  8 + enum AddressTypeEnum {
  9 + DEC = 'DEC',
  10 + HEX = 'HEX',
  11 + }
  12 +
  13 + const emit = defineEmits(['update:value']);
  14 +
  15 + const DEC_MAX_VALUE = parseInt('0xffff', 16);
  16 +
  17 + const props = defineProps<{
  18 + value?: string;
  19 + }>();
  20 +
  21 + const addressTypeOptions = [
  22 + { label: AddressTypeEnum.DEC, value: AddressTypeEnum.DEC },
  23 + { label: AddressTypeEnum.HEX, value: AddressTypeEnum.HEX },
  24 + ];
  25 +
  26 + const type = ref(AddressTypeEnum.DEC);
  27 +
  28 + const inputValue = ref<number | string>(0);
  29 +
  30 + const getHexValue = computed(() => {
  31 + return parseInt(unref(inputValue) || 0, 16);
  32 + });
  33 +
  34 + const getDecValue = computed(() => {
  35 + let formatValue = Number(unref(inputValue) || 0).toString(16);
  36 + formatValue = `0x${formatValue.padStart(4, '0')}`;
  37 + return (inputValue.value as number) > DEC_MAX_VALUE ? '0x0000' : formatValue;
  38 + });
  39 +
  40 + const handleEmit = () => {
  41 + const syncValue = unref(type) === AddressTypeEnum.DEC ? unref(getDecValue) : unref(getHexValue);
  42 + emit('update:value', syncValue);
  43 + };
  44 +
  45 + const handleChange = (value: AddressTypeEnum) => {
  46 + const syncValue = value === AddressTypeEnum.DEC ? unref(getHexValue) : unref(getDecValue);
  47 + inputValue.value = syncValue;
  48 + emit('update:value', syncValue);
  49 + };
  50 +
  51 + watch(
  52 + () => props.value,
  53 + (targetValue) => {
  54 + inputValue.value = targetValue || 0;
  55 + console.log(`inputValue: ${unref(inputValue)}`);
  56 + }
  57 + );
  58 +</script>
  59 +
  60 +<template>
  61 + <InputGroup compact class="!flex">
  62 + <Select
  63 + v-model:value="type"
  64 + :options="addressTypeOptions"
  65 + @change="handleChange"
  66 + class="bg-gray-200"
  67 + />
  68 + <InputNumber
  69 + v-if="type === AddressTypeEnum.DEC"
  70 + v-model:value="inputValue"
  71 + :step="1"
  72 + class="flex-1"
  73 + @change="handleEmit"
  74 + />
  75 + <Input v-if="type === AddressTypeEnum.HEX" v-model:value="inputValue" />
  76 + <div class="text-center h-8 leading-8 px-2 bg-gray-200 cursor-pointer rounded-1 w-20">
  77 + <div v-if="type === AddressTypeEnum.DEC">{{ getDecValue }}</div>
  78 + <div v-if="type === AddressTypeEnum.HEX">{{ getHexValue }}</div>
  79 + </div>
  80 + </InputGroup>
  81 +</template>
... ...
  1 +import { findDictItemByCode } from '/@/api/system/dict';
  2 +import { FormSchema, useComponentRegister } from '/@/components/Form';
  3 +import { DictEnum } from '/@/enums/dictEnum';
  4 +import RegisterAddressInput from './RegisterAddressInput.vue';
  5 +import { createPickerSearch } from '/@/utils/pickerSearch';
  6 +import { ControlGroup } from '../ControlGroup';
  7 +
  8 +export enum FormFieldsEnum {
  9 + ADDRESS = 'address',
  10 + FUNCTION_CODE = 'functionCode',
  11 + START_REGISTER_ADDRESS = 'startRegisterAddress',
  12 + REGISTER_NUMBER = 'registerNumber',
  13 + DATA_VALID = 'dataValid',
  14 +}
  15 +
  16 +useComponentRegister('RegisterAddressInput', RegisterAddressInput);
  17 +useComponentRegister('ControlGroup', ControlGroup);
  18 +
  19 +export const formSchemas: FormSchema[] = [
  20 + {
  21 + field: FormFieldsEnum.ADDRESS,
  22 + component: 'ApiSelect',
  23 + label: '从机地址',
  24 + componentProps: () => {
  25 + return {
  26 + api: async (params: Recordable) => {
  27 + try {
  28 + const result = await findDictItemByCode(params);
  29 + return result.map((item, index) => ({
  30 + ...item,
  31 + itemText: `${index + 1} - ${item.itemText}`,
  32 + }));
  33 + } catch (error) {
  34 + return [];
  35 + }
  36 + },
  37 + params: {
  38 + dictCode: DictEnum.SLAVE_ADDRESS,
  39 + },
  40 + labelField: 'itemText',
  41 + valueField: 'itemValue',
  42 + ...createPickerSearch(),
  43 + getPopupContainer: () => document.body,
  44 + };
  45 + },
  46 + },
  47 + {
  48 + field: FormFieldsEnum.FUNCTION_CODE,
  49 + component: 'ApiSelect',
  50 + label: '功能码',
  51 + componentProps: () => {
  52 + return {
  53 + api: findDictItemByCode,
  54 + params: {
  55 + dictCode: DictEnum.FUNCTION_CODE,
  56 + },
  57 + labelField: 'itemText',
  58 + valueField: 'itemValue',
  59 + getPopupContainer: () => document.body,
  60 + };
  61 + },
  62 + },
  63 + {
  64 + field: FormFieldsEnum.START_REGISTER_ADDRESS,
  65 + label: '起始寄存器地址',
  66 + component: 'RegisterAddressInput',
  67 + valueField: 'value',
  68 + changeEvent: 'update:value',
  69 + },
  70 + {
  71 + field: FormFieldsEnum.REGISTER_NUMBER,
  72 + label: '寄存器个数',
  73 + component: 'InputNumber',
  74 + componentProps: {
  75 + min: 1,
  76 + max: 64,
  77 + step: 1,
  78 + },
  79 + },
  80 + {
  81 + field: FormFieldsEnum.DATA_VALID,
  82 + label: '数据校验',
  83 + component: 'ApiSelect',
  84 + colProps: { span: 24 },
  85 + componentProps: () => {
  86 + return {
  87 + api: findDictItemByCode,
  88 + params: {
  89 + dictCode: DictEnum.DATA_VALIDATE,
  90 + },
  91 + labelField: 'itemText',
  92 + valueField: 'itemValue',
  93 + getPopupContainer: () => document.body,
  94 + };
  95 + },
  96 + },
  97 + // {
  98 + // field: 'test',
  99 + // label: '住',
  100 + // component: 'ControlGroup',
  101 + // },
  102 +];
... ...
  1 +export { default as CustomInput } 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 CreateTCPCommandModal from './CreateTCPCommandModal.vue';
  8 + import { useModal } from '/@/components/Modal';
  9 + import { ref } from 'vue';
  10 + import { unref } from 'vue';
  11 + import JSONEditorType from 'jsoneditor';
  12 +
  13 + const props = withDefaults(
  14 + defineProps<{
  15 + value?: any;
  16 + mode?: string;
  17 + inputProps?: Recordable;
  18 + showSettingAddonAfter?: boolean;
  19 + openSettingOnInputFocus?: boolean;
  20 + }>(),
  21 + {
  22 + mode: 'application/json',
  23 + showSettingAddonAfter: true,
  24 + openSettingOnInputFocus: false,
  25 + }
  26 + );
  27 +
  28 + const emit = defineEmits(['update:value']);
  29 +
  30 + const inputElRef = ref<Nullable<HTMLInputElement>>(null);
  31 +
  32 + const [registerCreateTCPCommandModal, { openModal }] = useModal();
  33 +
  34 + const getJSONValue = computed(() => {
  35 + return props.value;
  36 + });
  37 +
  38 + const handleEmit = (value: any) => {
  39 + emit('update:value', value);
  40 + openModal(false);
  41 + if (props.openSettingOnInputFocus) {
  42 + unref(inputElRef)?.blur();
  43 + }
  44 + };
  45 +
  46 + const handleClick = () => {
  47 + openModal(true);
  48 + };
  49 +
  50 + const handleFocus = () => {
  51 + if (props.openSettingOnInputFocus) {
  52 + openModal(true);
  53 + unref(inputElRef)?.blur();
  54 + }
  55 + };
  56 +
  57 + const handleEditorBlur = (_event: Event, instance: JSONEditorType) => {
  58 + const value = instance.getText();
  59 + handleEmit(value);
  60 + };
  61 +</script>
  62 +
  63 +<template>
  64 + <section>
  65 + <Input
  66 + v-if="mode === ModeEnum.NORMAL"
  67 + ref="inputElRef"
  68 + :value="getJSONValue"
  69 + @change="handleEmit"
  70 + v-bind="inputProps"
  71 + @focus="handleFocus"
  72 + >
  73 + <template v-if="showSettingAddonAfter" #addonAfter>
  74 + <SettingOutlined class="cursor-pointer" @click="handleClick" />
  75 + </template>
  76 + </Input>
  77 + <JSONEditor v-if="mode === ModeEnum.JSON" :value="getJSONValue" @blur="handleEditorBlur" />
  78 + <CreateTCPCommandModal @register="registerCreateTCPCommandModal" @update:value="handleEmit" />
  79 + </section>
  80 +</template>
... ...
  1 +import { TaskTargetEnum, TaskTargetNameEnum, TaskTypeEnum, TaskTypeNameEnum } from '../../config';
  2 +import { findDictItemByCode } from '/@/api/system/dict';
  3 +import { FormSchema, useComponentRegister } from '/@/components/Form';
  4 +import {
  5 + DevicePicker,
  6 + validateDevicePicker,
  7 + FormFieldsEnum as DeviceCascadePickerFieldsEnum,
  8 +} from '../DevicePicker';
  9 +import { CustomInput, ModeEnum } from '../CustomInput';
  10 +import { DeviceProfileModel } from '/@/api/device/model/deviceModel';
  11 +import { TransportTypeEnum } from '/@/views/device/profiles/components/TransportDescript/const';
  12 +import { getDeviceProfile } from '/@/api/alarm/position';
  13 +import { JSONEditorValidator } from '/@/components/CodeEditor/src/JSONEditor';
  14 +import { TimeUnitEnum, TimeUnitNameEnum } from '/@/enums/toolEnum';
  15 +import { createPickerSearch } from '/@/utils/pickerSearch';
  16 +
  17 +useComponentRegister('DevicePicker', DevicePicker);
  18 +useComponentRegister('CustomInput', CustomInput);
  19 +
  20 +export enum FormFieldsEnum {
  21 + // 任务名称
  22 + NAME = 'name',
  23 + // 目标类型
  24 + TARGET_TYPE = 'targetType',
  25 + // 设备类型选择
  26 + DEVICE_PROFILE = 'deviceProfile',
  27 + // 执行目标源
  28 + EXECUTE_TARGET_DATA = 'executeTargetData',
  29 + // 执行任务类型
  30 + EXECUTE_CONTENT_TYPE = 'executeContentType',
  31 + // 下发命令
  32 + RPC_COMMAND = 'rpcCommand',
  33 + // 推送方式
  34 + PUSH_WAY = 'pushWay',
  35 + // 执行周期类型
  36 + EXECUTE_TIME_TYPE = 'executeTimeType',
  37 + // 执行间隔时间
  38 + EXECUTE_TIME_INTERVAL = 'interval',
  39 + // 执行周期类型
  40 + EXECUTE_TIME_PERIOD_TYPE = 'periodType',
  41 + // 周期 每月 每周
  42 + EXECUTE_TIME_PERIOD = 'period',
  43 + // time时间
  44 + TIME = 'time',
  45 + // 设备传输协议
  46 + TRANSPORT_TYPE = 'transportType',
  47 + // 间隔时间单位
  48 + POLL_UNIT = 'pollUnit',
  49 +}
  50 +
  51 +export enum PushWayEnum {
  52 + MQTT = 'MQTT',
  53 + TCP = 'TCP',
  54 +}
  55 +
  56 +export enum ExecuteTimeTypeEnum {
  57 + CUSTOM = 'CUSTOM',
  58 + POLL = 'POLL',
  59 +}
  60 +
  61 +export enum ExecuteTimeTypeNameEnum {
  62 + CUSTOM = '自定义',
  63 + POLL = '间隔时间重复',
  64 +}
  65 +
  66 +export enum PeriodTypeEnum {
  67 + MONTH,
  68 + WEEK,
  69 + DAY,
  70 +}
  71 +
  72 +export enum PeriodTypeNameEnum {
  73 + MONTH = '每月',
  74 + WEEK = '每周',
  75 + DAY = '每日',
  76 +}
  77 +
  78 +const isShowCustomIntervalTimeSetting = (model: Recordable, flag: PeriodTypeEnum) => {
  79 + return (
  80 + model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.CUSTOM &&
  81 + model[FormFieldsEnum.EXECUTE_TIME_PERIOD_TYPE] === flag
  82 + );
  83 +};
  84 +
  85 +export const formSchemas: FormSchema[] = [
  86 + {
  87 + field: FormFieldsEnum.NAME,
  88 + component: 'Input',
  89 + label: '任务名称',
  90 + rules: [{ required: true, message: '请填写任务名称' }],
  91 + componentProps: {
  92 + placeholder: '请输入任务名称',
  93 + },
  94 + },
  95 + {
  96 + field: FormFieldsEnum.TARGET_TYPE,
  97 + component: 'RadioGroup',
  98 + label: '目标类型',
  99 + defaultValue: TaskTargetEnum.DEVICES,
  100 + helpMessage: ['执行任务的目标设备,可以是多个指定的设备,也可以是一个设备类型下的所有设备.'],
  101 + componentProps: {
  102 + options: [
  103 + { label: TaskTargetNameEnum.DEVICES, value: TaskTargetEnum.DEVICES },
  104 + { label: TaskTargetNameEnum.PRODUCTS, value: TaskTargetEnum.PRODUCTS },
  105 + ],
  106 + },
  107 + },
  108 + {
  109 + field: FormFieldsEnum.DEVICE_PROFILE,
  110 + component: 'ApiSelect',
  111 + label: '产品',
  112 + ifShow: ({ model }) => model[FormFieldsEnum.TARGET_TYPE] === TaskTargetEnum.PRODUCTS,
  113 + componentProps: ({ formActionType }) => {
  114 + const { setFieldsValue } = formActionType;
  115 + return {
  116 + api: getDeviceProfile,
  117 + placeholder: '请选择产品',
  118 + labelField: 'name',
  119 + valueField: 'id',
  120 + onChange(value: string, option: DeviceProfileModel) {
  121 + if (value) {
  122 + const isTCP = option.transportType === TransportTypeEnum.TCP;
  123 + setFieldsValue({
  124 + [FormFieldsEnum.TRANSPORT_TYPE]: isTCP ? PushWayEnum.TCP : PushWayEnum.MQTT,
  125 + [FormFieldsEnum.PUSH_WAY]: isTCP ? PushWayEnum.TCP : PushWayEnum.MQTT,
  126 + ...(isTCP ? {} : { [FormFieldsEnum.EXECUTE_CONTENT_TYPE]: TaskTypeEnum.CUSTOM }),
  127 + });
  128 + }
  129 + },
  130 + ...createPickerSearch(),
  131 + getPopupContainer: () => document.body,
  132 + };
  133 + },
  134 + },
  135 + {
  136 + field: FormFieldsEnum.EXECUTE_TARGET_DATA,
  137 + component: 'DevicePicker',
  138 + label: '设备',
  139 + helpMessage: ['任务可以对目标设备按预设的时间策略执行任务。'],
  140 + ifShow: ({ model }) => model[FormFieldsEnum.TARGET_TYPE] === TaskTargetEnum.DEVICES,
  141 + rules: [validateDevicePicker()],
  142 + valueField: 'value',
  143 + changeEvent: 'update:value',
  144 + componentProps: ({ formActionType }) => {
  145 + const { setFieldsValue } = formActionType;
  146 + return {
  147 + multiple: true,
  148 + onChange(key: string, _value: string | string[], option: DeviceProfileModel) {
  149 + if (key === DeviceCascadePickerFieldsEnum.DEVICE_PROFILE) {
  150 + const isTCP = (option || {}).transportType === TransportTypeEnum.TCP;
  151 + setFieldsValue({
  152 + [FormFieldsEnum.TRANSPORT_TYPE]: _value ? option.transportType : null,
  153 + [FormFieldsEnum.PUSH_WAY]: isTCP ? PushWayEnum.TCP : PushWayEnum.MQTT,
  154 + ...(isTCP ? {} : { [FormFieldsEnum.EXECUTE_CONTENT_TYPE]: TaskTypeEnum.CUSTOM }),
  155 + });
  156 + }
  157 + },
  158 + };
  159 + },
  160 + },
  161 + {
  162 + field: FormFieldsEnum.TRANSPORT_TYPE,
  163 + component: 'Input',
  164 + label: '',
  165 + show: false,
  166 + },
  167 + {
  168 + field: FormFieldsEnum.EXECUTE_CONTENT_TYPE,
  169 + component: 'RadioGroup',
  170 + label: '任务类型',
  171 + defaultValue: TaskTypeEnum.CUSTOM,
  172 + componentProps: ({ formActionType, formModel }) => {
  173 + const { setFieldsValue } = formActionType;
  174 + const transportType = Reflect.get(formModel, FormFieldsEnum.TRANSPORT_TYPE);
  175 + return {
  176 + options: [
  177 + {
  178 + label: TaskTypeNameEnum.CUSTOM,
  179 + value: TaskTypeEnum.CUSTOM,
  180 + },
  181 + {
  182 + label: TaskTypeNameEnum.MODBUS_RTU,
  183 + value: TaskTypeEnum.MODBUS_RTU,
  184 + disabled: transportType && transportType !== PushWayEnum.TCP,
  185 + },
  186 + ],
  187 + onChange(value: TaskTypeEnum) {
  188 + value &&
  189 + setFieldsValue({
  190 + [FormFieldsEnum.RPC_COMMAND]: '',
  191 + });
  192 + },
  193 + };
  194 + },
  195 + },
  196 + {
  197 + field: FormFieldsEnum.PUSH_WAY,
  198 + component: 'RadioGroup',
  199 + label: '推送方式',
  200 + defaultValue: PushWayEnum.MQTT,
  201 + show: false,
  202 + componentProps: {
  203 + options: [
  204 + { label: PushWayEnum.MQTT, value: PushWayEnum.MQTT },
  205 + { label: PushWayEnum.TCP, value: PushWayEnum.TCP },
  206 + ],
  207 + },
  208 + },
  209 + {
  210 + field: FormFieldsEnum.RPC_COMMAND,
  211 + component: 'CustomInput',
  212 + label: '自定义数据流',
  213 + rules: [{ required: true, message: '请输入自定义数据流' }],
  214 + dynamicRules: ({ model }) =>
  215 + model[FormFieldsEnum.PUSH_WAY] === PushWayEnum.MQTT ? JSONEditorValidator() : [],
  216 + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_CONTENT_TYPE] === TaskTypeEnum.CUSTOM,
  217 + valueField: 'value',
  218 + changeEvent: 'update:value',
  219 + componentProps: ({ formModel }) => {
  220 + const pushMode = Reflect.get(formModel, FormFieldsEnum.PUSH_WAY);
  221 + return {
  222 + inputProps: {
  223 + placeholder: '请输入自定义数据流',
  224 + },
  225 + mode: pushMode === PushWayEnum.MQTT ? ModeEnum.JSON : ModeEnum.NORMAL,
  226 + };
  227 + },
  228 + },
  229 + {
  230 + field: FormFieldsEnum.RPC_COMMAND,
  231 + component: 'CustomInput',
  232 + label: 'ModbusRTU轮询',
  233 + rules: [{ required: true, message: '请输入Modbus RTU 轮询指令' }],
  234 + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_CONTENT_TYPE] === TaskTypeEnum.MODBUS_RTU,
  235 + valueField: 'value',
  236 + changeEvent: 'update:value',
  237 + componentProps: () => {
  238 + return {
  239 + inputProps: {
  240 + placeholder: '请输入Modbus RTU 轮询指令',
  241 + },
  242 + showSettingAddonAfter: false,
  243 + openSettingOnInputFocus: true,
  244 + mode: ModeEnum.NORMAL,
  245 + };
  246 + },
  247 + },
  248 + {
  249 + field: FormFieldsEnum.EXECUTE_TIME_TYPE,
  250 + label: '任务定时设置',
  251 + component: 'RadioGroup',
  252 + defaultValue: ExecuteTimeTypeEnum.POLL,
  253 + componentProps: {
  254 + options: [
  255 + { label: ExecuteTimeTypeNameEnum.POLL, value: ExecuteTimeTypeEnum.POLL },
  256 + { label: ExecuteTimeTypeNameEnum.CUSTOM, value: ExecuteTimeTypeEnum.CUSTOM },
  257 + ],
  258 + },
  259 + },
  260 + {
  261 + field: FormFieldsEnum.EXECUTE_TIME_PERIOD_TYPE,
  262 + component: 'Select',
  263 + label: '周期',
  264 + required: true,
  265 + defaultValue: PeriodTypeEnum.MONTH,
  266 + componentProps: {
  267 + placeholder: '请选择周期',
  268 + options: [
  269 + { label: PeriodTypeNameEnum.DAY, value: PeriodTypeEnum.DAY },
  270 + { label: PeriodTypeNameEnum.WEEK, value: PeriodTypeEnum.WEEK },
  271 + { label: PeriodTypeNameEnum.MONTH, value: PeriodTypeEnum.MONTH },
  272 + ],
  273 + getPopupContainer: () => document.body,
  274 + },
  275 + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.CUSTOM,
  276 + },
  277 + {
  278 + field: FormFieldsEnum.EXECUTE_TIME_PERIOD,
  279 + component: 'ApiSelect',
  280 + label: '每月',
  281 + required: true,
  282 + componentProps: {
  283 + placeholder: '请选择月份',
  284 + api: findDictItemByCode,
  285 + params: {
  286 + dictCode: 'every_month',
  287 + },
  288 + labelField: 'itemText',
  289 + valueField: 'itemValue',
  290 + getPopupContainer: () => document.body,
  291 + },
  292 + ifShow: ({ model }) => isShowCustomIntervalTimeSetting(model, PeriodTypeEnum.MONTH),
  293 + },
  294 + {
  295 + field: FormFieldsEnum.EXECUTE_TIME_PERIOD,
  296 + component: 'ApiSelect',
  297 + label: '每周',
  298 + required: true,
  299 + componentProps: {
  300 + placeholder: '请选择周期',
  301 + api: findDictItemByCode,
  302 + params: {
  303 + dictCode: 'every_week',
  304 + },
  305 + labelField: 'itemText',
  306 + valueField: 'itemValue',
  307 + getPopupContainer: () => document.body,
  308 + },
  309 + ifShow: ({ model }) => isShowCustomIntervalTimeSetting(model, PeriodTypeEnum.WEEK),
  310 + },
  311 + {
  312 + field: FormFieldsEnum.TIME,
  313 + component: 'TimePicker',
  314 + label: '时间',
  315 + required: true,
  316 + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.CUSTOM,
  317 + componentProps: {
  318 + getPopupContainer: () => document.body,
  319 + valueFormat: 'HH:mm:ss',
  320 + },
  321 + },
  322 + {
  323 + field: FormFieldsEnum.POLL_UNIT,
  324 + component: 'RadioGroup',
  325 + label: '时间单位',
  326 + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.POLL,
  327 + defaultValue: TimeUnitEnum.SECOND,
  328 + componentProps: {
  329 + options: [
  330 + { label: TimeUnitNameEnum.SECOND, value: TimeUnitEnum.SECOND },
  331 + { label: TimeUnitNameEnum.MINUTE, value: TimeUnitEnum.MINUTE },
  332 + { label: TimeUnitNameEnum.HOUR, value: TimeUnitEnum.HOUR },
  333 + ],
  334 + },
  335 + },
  336 + {
  337 + field: FormFieldsEnum.EXECUTE_TIME_INTERVAL,
  338 + label: '间隔时间',
  339 + component: 'InputNumber',
  340 + ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.POLL,
  341 + componentProps: ({ formModel }) => {
  342 + const unit = formModel[FormFieldsEnum.POLL_UNIT];
  343 + return {
  344 + min: 0,
  345 + max: unit === TimeUnitEnum.HOUR ? 23 : 59,
  346 + step: 1,
  347 + placeholder: '请输入间隔时间',
  348 + };
  349 + },
  350 + },
  351 +];
... ...
  1 +export { default as DetailModal } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { BasicForm, useForm } from '/@/components/Form';
  3 + import { BasicModal, useModalInner } from '/@/components/Modal';
  4 + import { formSchemas } from './config';
  5 + import { ref } from 'vue';
  6 + import { composeData, parseData } from './util';
  7 + import { createTask, updateTask } from '/@/api/task';
  8 + import { ModalParamsType } from '/#/utils';
  9 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  10 + import { TaskRecordType } from '/@/api/task/model';
  11 + import { FormValueType } from './util';
  12 + import { unref } from 'vue';
  13 +
  14 + const props = defineProps<{
  15 + reload: Fn;
  16 + }>();
  17 + defineEmits(['register']);
  18 +
  19 + const formMode = ref(DataActionModeEnum.CREATE);
  20 +
  21 + const dataSource = ref<TaskRecordType>();
  22 + const [registerModal, { closeModal }] = useModalInner(
  23 + (
  24 + { record, mode }: ModalParamsType<TaskRecordType> = {
  25 + record: {} as unknown as TaskRecordType,
  26 + mode: DataActionModeEnum.CREATE,
  27 + }
  28 + ) => {
  29 + dataSource.value = record;
  30 + formMode.value = mode;
  31 + resetFields();
  32 + if (record && mode === DataActionModeEnum.UPDATE) {
  33 + const res = parseData(record);
  34 + console.log({ record, res });
  35 + setFieldsValue({ ...res });
  36 + }
  37 + }
  38 + );
  39 +
  40 + const [registerForm, { getFieldsValue, validate, setFieldsValue, resetFields }] = useForm({
  41 + schemas: formSchemas,
  42 + showActionButtonGroup: false,
  43 + layout: 'inline',
  44 + baseColProps: { span: 24 },
  45 + labelWidth: 140,
  46 + });
  47 +
  48 + const loading = ref(false);
  49 + const handleOk = async () => {
  50 + try {
  51 + loading.value = true;
  52 + await validate();
  53 + const res = getFieldsValue();
  54 + const _res = composeData(res as Required<FormValueType>);
  55 + formMode.value === DataActionModeEnum.CREATE
  56 + ? await createTask(_res)
  57 + : await updateTask({ ..._res, id: unref(dataSource)?.id as string });
  58 + closeModal();
  59 + props.reload?.();
  60 + } catch (error) {
  61 + throw error;
  62 + } finally {
  63 + loading.value = false;
  64 + }
  65 + };
  66 +</script>
  67 +
  68 +<template>
  69 + <BasicModal
  70 + @register="registerModal"
  71 + title="创建任务"
  72 + width="700px"
  73 + :okButtonProps="{ loading }"
  74 + @ok="handleOk"
  75 + >
  76 + <BasicForm @register="registerForm" class="form-container" />
  77 + </BasicModal>
  78 +</template>
  79 +
  80 +<style lang="less" scoped>
  81 + .form-container {
  82 + :deep(.ant-input-number) {
  83 + width: 100%;
  84 + }
  85 + }
  86 +</style>
... ...
  1 +import CronParser, { CronFields, HourRange, SixtyRange } from 'cron-parser';
  2 +import { dateUtil } from '/@/utils/dateUtil';
  3 +import { ExecuteTimeTypeEnum, FormFieldsEnum, PushWayEnum } from './config';
  4 +import { CreateTaskRecordType, TaskRecordType } from '/@/api/task/model';
  5 +import { DeviceCascadePickerValueType } from '../DevicePicker';
  6 +import { TaskTargetEnum } from '../../config';
  7 +import { TimeUnitEnum } from '/@/enums/toolEnum';
  8 +
  9 +export interface FormValueType
  10 + extends Partial<Record<Exclude<FormFieldsEnum, FormFieldsEnum.TRANSPORT_TYPE>, any>> {
  11 + [FormFieldsEnum.EXECUTE_TARGET_DATA]: DeviceCascadePickerValueType;
  12 +}
  13 +
  14 +type CanWrite<T> = {
  15 + -readonly [K in keyof T]: T[K];
  16 +};
  17 +
  18 +interface GenCronExpressionResultType {
  19 + effective: boolean;
  20 + expression?: string;
  21 +}
  22 +
  23 +export const usePluginGenCronExpression = (
  24 + time: string,
  25 + expression = '* * * * * * *',
  26 + includesYear = true
  27 +): GenCronExpressionResultType => {
  28 + try {
  29 + const separator = ' ';
  30 + const removeYear = expression.split(separator).slice(0, 6).join(separator);
  31 +
  32 + const date = dateUtil(time, 'HH:mm:ss');
  33 +
  34 + const second = date.get('second') as SixtyRange;
  35 + const minute = date.get('minute') as SixtyRange;
  36 + const hour = date.get('hour') as HourRange;
  37 +
  38 + const result = CronParser.parseExpression(removeYear, { utc: true, nthDayOfWeek: 4 });
  39 + const fields = JSON.parse(JSON.stringify(result.fields)) as CanWrite<CronFields>;
  40 + fields.second = [second];
  41 + fields.minute = [minute];
  42 + fields.hour = [hour];
  43 +
  44 + // console.log(CronParser.fieldsToExpression(CronParser.parseExpression('').fields).stringify());
  45 +
  46 + let newExpression = CronParser.fieldsToExpression(fields).stringify(true);
  47 + newExpression = includesYear ? `${newExpression} *` : newExpression;
  48 + return { effective: true, expression: newExpression };
  49 + } catch (error) {
  50 + // throw error;
  51 + return { effective: false };
  52 + }
  53 +};
  54 +
  55 +export const genCronExpression = (
  56 + time: string,
  57 + expression = '* * * * * * *'
  58 +): GenCronExpressionResultType => {
  59 + try {
  60 + const separator = ' ';
  61 + const list: (string | number)[] = expression.split(separator);
  62 +
  63 + const date = dateUtil(time, 'HH:mm:ss');
  64 +
  65 + const second = date.get('second') as SixtyRange;
  66 + const minute = date.get('minute') as SixtyRange;
  67 + const hour = date.get('hour') as HourRange;
  68 +
  69 + list[0] = second;
  70 + list[1] = minute;
  71 + list[2] = hour;
  72 +
  73 + return { effective: true, expression: list.join(separator) };
  74 + } catch (error) {
  75 + // throw error;
  76 + return { effective: false };
  77 + }
  78 +};
  79 +
  80 +export const composeData = (result: Required<FormValueType>): CreateTaskRecordType => {
  81 + const {
  82 + name,
  83 + targetType,
  84 + rpcCommand,
  85 + pushWay,
  86 + executeContentType,
  87 + executeTargetData,
  88 + deviceProfile,
  89 + executeTimeType,
  90 + period,
  91 + periodType,
  92 + time,
  93 + interval,
  94 + pollUnit,
  95 + } = result;
  96 +
  97 + const { organizationId, deviceType, deviceId, deviceProfileId } = executeTargetData || {};
  98 +
  99 + const { expression } = genCronExpression(time, period);
  100 +
  101 + const cron =
  102 + executeTimeType === ExecuteTimeTypeEnum.POLL ? `0/${interval} * * * * ? *` : expression!;
  103 +
  104 + return {
  105 + name,
  106 + targetType,
  107 + executeContent: {
  108 + pushContent: {
  109 + rpcCommand: pushWay === PushWayEnum.MQTT ? JSON.parse(rpcCommand) : pushWay,
  110 + },
  111 + pushWay,
  112 + type: executeContentType,
  113 + },
  114 + executeTarget: {
  115 + data: targetType === TaskTargetEnum.PRODUCTS ? [deviceProfile] : (deviceId as string[]),
  116 + deviceType,
  117 + organizationId,
  118 + deviceProfileId: targetType === TaskTargetEnum.PRODUCTS ? deviceProfile : deviceProfileId,
  119 + },
  120 + executeTime: {
  121 + type: executeTimeType,
  122 + periodType,
  123 + period,
  124 + time: executeTimeType === ExecuteTimeTypeEnum.POLL ? interval : time,
  125 + cron,
  126 + pollUnit,
  127 + },
  128 + };
  129 +};
  130 +
  131 +export const parseData = (result: TaskRecordType): Required<FormValueType> => {
  132 + const { name, targetType, executeContent, executeTarget, executeTime } = result;
  133 + const { pushContent: { rpcCommand } = {}, pushWay, type: executeContentType } = executeContent;
  134 + const { data, deviceProfileId, deviceType, organizationId } = executeTarget as Required<
  135 + TaskRecordType['executeTarget']
  136 + >;
  137 + const { type: executeTimeType, period, periodType, time, pollUnit } = executeTime;
  138 + return {
  139 + name,
  140 + targetType,
  141 + rpcCommand: pushWay === PushWayEnum.MQTT ? JSON.stringify(rpcCommand) : rpcCommand,
  142 + pushWay,
  143 + executeContentType,
  144 + executeTargetData: {
  145 + deviceId: targetType === TaskTargetEnum.DEVICES ? data : [],
  146 + deviceProfileId,
  147 + deviceType,
  148 + organizationId,
  149 + },
  150 + deviceProfile: targetType === TaskTargetEnum.PRODUCTS ? data : null,
  151 + executeTimeType,
  152 + period,
  153 + periodType: executeTimeType === ExecuteTimeTypeEnum.CUSTOM ? periodType : null,
  154 + time: executeTimeType === ExecuteTimeTypeEnum.CUSTOM ? time : null,
  155 + interval: executeTimeType === ExecuteTimeTypeEnum.POLL ? time : null,
  156 + pollUnit: executeTimeType === ExecuteTimeTypeEnum.POLL ? pollUnit : TimeUnitEnum.SECOND,
  157 + };
  158 +};
... ...
  1 +export { default as DevicePicker } from './index.vue';
  2 +export { validateDevicePicker } from './utils';
  3 +
  4 +export enum FormFieldsEnum {
  5 + DEVICE_TYPE = 'deviceType',
  6 + DEVICE_PROFILE = 'deviceProfileId',
  7 + ORGANIZATION = 'organizationId',
  8 + DEVICE = 'deviceId',
  9 +}
  10 +
  11 +export interface DeviceCascadePickerValueType
  12 + extends Record<Exclude<FormFieldsEnum, FormFieldsEnum.DEVICE>, string> {
  13 + [FormFieldsEnum.DEVICE]: string | string[];
  14 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { h } from 'vue';
  3 + import { getMeetTheConditionsDevice } from '/@/api/dataBoard';
  4 + import { getOrganizationList } from '/@/api/system/system';
  5 + import { BasicForm, useForm } from '/@/components/Form';
  6 + import { copyTransFun } from '/@/utils/fnUtils';
  7 + import { Tooltip } from 'ant-design-vue';
  8 + import { watch } from 'vue';
  9 + import { nextTick } from 'vue';
  10 + import { getDeviceProfile } from '/@/api/alarm/position';
  11 + import { findDictItemByCode } from '/@/api/system/dict';
  12 + import { DictEnum } from '/@/enums/dictEnum';
  13 + import { FormFieldsEnum } from '.';
  14 + import { isObject } from '/@/utils/is';
  15 + import { createPickerSearch } from '/@/utils/pickerSearch';
  16 +
  17 + const props = withDefaults(
  18 + defineProps<{
  19 + value?: Recordable;
  20 + multiple?: boolean;
  21 + max?: 3;
  22 + }>(),
  23 + {
  24 + value: () => ({}),
  25 + multiple: false,
  26 + }
  27 + );
  28 +
  29 + const emit = defineEmits(['update:value', 'change']);
  30 +
  31 + const handleChange = (key: string, value: string | string[], option: Recordable) => {
  32 + emit('change', key, value, option);
  33 + };
  34 +
  35 + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
  36 + const createMaxTagPlaceholder = (omittedValues: Record<'label' | 'value', string>[]) => {
  37 + return h(
  38 + Tooltip,
  39 + {},
  40 + {
  41 + default: () => h('span', `+${omittedValues.length}...`),
  42 + title: () => h('span', 'other'),
  43 + }
  44 + );
  45 + };
  46 +
  47 + const handleEmit = async (key: string, value: string | string[], option: Recordable) => {
  48 + await nextTick();
  49 + let _value = getFieldsValue();
  50 + _value = {
  51 + ..._value,
  52 +
  53 + ...(key === FormFieldsEnum.DEVICE_TYPE
  54 + ? {
  55 + [FormFieldsEnum.DEVICE_PROFILE]: null,
  56 + [FormFieldsEnum.ORGANIZATION]: null,
  57 + [FormFieldsEnum.DEVICE]: props.multiple ? [] : null,
  58 + }
  59 + : {}),
  60 +
  61 + ...(key === FormFieldsEnum.DEVICE_PROFILE
  62 + ? {
  63 + [FormFieldsEnum.ORGANIZATION]: null,
  64 + [FormFieldsEnum.DEVICE]: props.multiple ? [] : null,
  65 + }
  66 + : {}),
  67 +
  68 + ...(key === FormFieldsEnum.ORGANIZATION
  69 + ? {
  70 + [FormFieldsEnum.DEVICE]: props.multiple ? [] : null,
  71 + }
  72 + : {}),
  73 + };
  74 + handleChange(key, value, option);
  75 + emit('update:value', { ..._value });
  76 + };
  77 + const [register, { setFieldsValue, getFieldsValue, resetFields }] = useForm({
  78 + schemas: [
  79 + {
  80 + field: FormFieldsEnum.DEVICE_TYPE,
  81 + component: 'ApiSelect',
  82 + label: '',
  83 + componentProps: () => {
  84 + return {
  85 + api: findDictItemByCode,
  86 + params: {
  87 + dictCode: DictEnum.DEVICE_TYPE,
  88 + },
  89 + labelField: 'itemText',
  90 + valueField: 'itemValue',
  91 + placeholder: '请选择设备类型',
  92 + onChange(value: string, option: Recordable) {
  93 + handleEmit(FormFieldsEnum.DEVICE_TYPE, value, option);
  94 + },
  95 + getPopupContainer: () => document.body,
  96 + ...createPickerSearch(),
  97 + };
  98 + },
  99 + },
  100 + {
  101 + field: FormFieldsEnum.DEVICE_PROFILE,
  102 + component: 'ApiSelect',
  103 + label: '',
  104 + componentProps: ({ formModel }) => {
  105 + const deviceType = Reflect.get(formModel, FormFieldsEnum.DEVICE_TYPE);
  106 + return {
  107 + api: async () => {
  108 + try {
  109 + return await getDeviceProfile(deviceType);
  110 + } catch (error) {
  111 + return [];
  112 + }
  113 + },
  114 + placeholder: '请选择产品',
  115 + labelField: 'name',
  116 + valueField: 'id',
  117 + onChange(value: string, options: Recordable) {
  118 + handleEmit(FormFieldsEnum.DEVICE_PROFILE, value, options);
  119 + },
  120 + getPopupContainer: () => document.body,
  121 + ...createPickerSearch(),
  122 + };
  123 + },
  124 + },
  125 + {
  126 + field: FormFieldsEnum.ORGANIZATION,
  127 + component: 'ApiTreeSelect',
  128 + label: '',
  129 + componentProps: () => {
  130 + return {
  131 + api: async () => {
  132 + try {
  133 + const data = await getOrganizationList();
  134 + copyTransFun(data as any);
  135 + return data;
  136 + } catch (error) {
  137 + console.log(error);
  138 + return [];
  139 + }
  140 + },
  141 + placeholder: '请选择组织',
  142 + labelField: 'name',
  143 + valueField: 'id',
  144 + childField: 'children',
  145 + onChange(value: string, option: Recordable) {
  146 + handleEmit(FormFieldsEnum.ORGANIZATION, value, option);
  147 + },
  148 + getPopupContainer: () => document.body,
  149 + };
  150 + },
  151 + },
  152 + {
  153 + field: FormFieldsEnum.DEVICE,
  154 + label: '',
  155 + component: 'ApiSelect',
  156 + componentProps: ({ formModel }) => {
  157 + const deviceType = Reflect.get(formModel, FormFieldsEnum.DEVICE_TYPE);
  158 + const deviceProfileId = Reflect.get(formModel, FormFieldsEnum.DEVICE_PROFILE);
  159 + const organizationId = Reflect.get(formModel, FormFieldsEnum.ORGANIZATION);
  160 + return {
  161 + api: async () => {
  162 + try {
  163 + if (!organizationId) return [];
  164 + return await getMeetTheConditionsDevice({
  165 + deviceProfileId,
  166 + deviceType,
  167 + organizationId,
  168 + });
  169 + } catch (error) {
  170 + return [];
  171 + }
  172 + },
  173 + mode: props.multiple ? 'multiple' : 'combobox',
  174 + labelField: 'name',
  175 + valueField: 'tbDeviceId',
  176 + placeholder: '请选择设备',
  177 + maxTagCount: 3,
  178 + maxTagTextLength: 4,
  179 + onChange(value: string, option: Recordable) {
  180 + handleEmit(FormFieldsEnum.DEVICE, value, option);
  181 + },
  182 + getPopupContainer: () => document.body,
  183 + ...createPickerSearch(),
  184 + };
  185 + },
  186 + },
  187 + ],
  188 + showActionButtonGroup: false,
  189 + layout: 'inline',
  190 + baseColProps: { span: 6 },
  191 + });
  192 +
  193 + const setValue = async () => {
  194 + await nextTick();
  195 + if (!props.value || (isObject(props.value) && !Object.values(props.value).length)) {
  196 + resetFields();
  197 + return;
  198 + }
  199 + setFieldsValue(props.value);
  200 + };
  201 +
  202 + watch(
  203 + () => props.value,
  204 + () => {
  205 + setValue();
  206 + },
  207 + {
  208 + immediate: true,
  209 + }
  210 + );
  211 +</script>
  212 +
  213 +<template>
  214 + <BasicForm @register="register" class="device-picker" />
  215 +</template>
  216 +
  217 +<style lang="less" scoped>
  218 + .device-picker {
  219 + :deep(.ant-row) {
  220 + width: 100%;
  221 + }
  222 +
  223 + :deep(.ant-form-item-control-input-content) {
  224 + div > div {
  225 + width: 100%;
  226 + }
  227 + }
  228 + }
  229 +</style>
... ...
  1 +import { FormFieldsEnum } from '.';
  2 +import { Rule } from '/@/components/Form';
  3 +
  4 +export const validateDevicePicker = () => {
  5 + return {
  6 + required: true,
  7 + validateTrigger: 'blur',
  8 + validator(_rule: Recordable, value: Recordable, _callback: Fn) {
  9 + const device = Reflect.get(value || {}, FormFieldsEnum.DEVICE);
  10 + if (!device) return Promise.reject('请选择设备');
  11 + return Promise.resolve();
  12 + },
  13 + } as Rule;
  14 +};
... ...
  1 +export { default as TaskCard } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { Button, Card, Switch, Tooltip } from 'ant-design-vue';
  3 + import {
  4 + CloudSyncOutlined,
  5 + PlayCircleOutlined,
  6 + QuestionCircleOutlined,
  7 + } from '@ant-design/icons-vue';
  8 + import { TaskRecordType } from '/@/api/task/model';
  9 + import { StateEnum, TaskTargetNameEnum, TaskTypeEnum } from '../../config';
  10 + import { TaskTypeNameEnum } from '../../config';
  11 + import AuthDropDown from '/@/components/Widget/AuthDropDown.vue';
  12 + import { useMessage } from '/@/hooks/web/useMessage';
  13 + import { cancelTask, deleteTask, updateState } from '/@/api/task';
  14 + import { computed } from '@vue/reactivity';
  15 + import { unref } from 'vue';
  16 + import { ref } from 'vue';
  17 +
  18 + enum DropMenuEvent {
  19 + DELETE = 'DELETE',
  20 + EDIT = 'EDIT',
  21 + }
  22 +
  23 + const props = withDefaults(
  24 + defineProps<{
  25 + record: TaskRecordType;
  26 + reload: Fn;
  27 + deviceTaskCardMode?: boolean;
  28 + tbDeviceId?: string;
  29 + }>(),
  30 + {
  31 + deviceTaskCardMode: false,
  32 + }
  33 + );
  34 +
  35 + const emit = defineEmits(['edit']);
  36 +
  37 + const loading = ref(false);
  38 +
  39 + const { createMessage } = useMessage();
  40 +
  41 + const getRecord = computed(() => {
  42 + return props.record;
  43 + });
  44 +
  45 + const getCancelState = computed(() => {
  46 + const { executeTarget } = unref(getRecord);
  47 + const { cancelExecuteDevices } = executeTarget;
  48 + return !!(cancelExecuteDevices && cancelExecuteDevices.length);
  49 + });
  50 +
  51 + const handleDelete = async () => {
  52 + try {
  53 + await deleteTask([unref(getRecord).id]);
  54 + createMessage.success('删除成功');
  55 + props.reload?.();
  56 + } catch (error) {}
  57 + };
  58 +
  59 + const handleSwitchState = async () => {
  60 + try {
  61 + loading.value = true;
  62 + await updateState(
  63 + unref(getRecord).id,
  64 + !unref(getRecord).state ? StateEnum.ENABLE : StateEnum.CLOSE
  65 + );
  66 + createMessage.success('更新状态成功');
  67 + props.reload?.();
  68 + } catch (error) {
  69 + throw error;
  70 + } finally {
  71 + loading.value = false;
  72 + }
  73 + };
  74 +
  75 + const handleCancelTask = async () => {
  76 + try {
  77 + if (!props.tbDeviceId) return;
  78 + loading.value = true;
  79 + await cancelTask({ id: unref(getRecord).id, tbDeviceId: props.tbDeviceId });
  80 + createMessage.success('设置成功');
  81 + props.reload?.();
  82 + } catch (error) {
  83 + throw error;
  84 + } finally {
  85 + loading.value = false;
  86 + }
  87 + };
  88 +</script>
  89 +
  90 +<template>
  91 + <Card hoverable class="card-container !rounded">
  92 + <div v-if="!deviceTaskCardMode" class="flex justify-end mb-4">
  93 + <AuthDropDown
  94 + @click.stop
  95 + :trigger="['hover']"
  96 + :drop-menu-list="[
  97 + {
  98 + text: '编辑',
  99 + event: DropMenuEvent.DELETE,
  100 + icon: 'ant-design:edit-outlined',
  101 + onClick: emit.bind(null, 'edit', getRecord),
  102 + },
  103 + {
  104 + text: '删除',
  105 + event: DropMenuEvent.DELETE,
  106 + icon: 'ant-design:delete-outlined',
  107 + popconfirm: {
  108 + title: '是否确认删除操作?',
  109 + onConfirm: handleDelete.bind(null),
  110 + },
  111 + },
  112 + ]"
  113 + />
  114 + </div>
  115 + <div class="flex text-base font-medium justify-between mb-2">
  116 + <Tooltip :title="getRecord.name" placement="topLeft">
  117 + <div class="truncate max-w-48">{{ getRecord.name }}</div>
  118 + </Tooltip>
  119 + <span
  120 + v-if="deviceTaskCardMode"
  121 + class="text-xs w-12 leading-6 text-right"
  122 + :class="getRecord.state === StateEnum.ENABLE ? 'text-green-500' : 'text-red-500'"
  123 + >
  124 + {{ getRecord.state === StateEnum.ENABLE ? '已启用' : '未启用' }}
  125 + </span>
  126 + <Switch
  127 + v-if="!deviceTaskCardMode"
  128 + :checked="getRecord.state === StateEnum.ENABLE"
  129 + :loading="loading"
  130 + size="small"
  131 + @click="handleSwitchState"
  132 + />
  133 + </div>
  134 + <div class="flex gap-2 items-center">
  135 + <div
  136 + class="rounded flex justify-center items-center w-6 h-6"
  137 + :class="
  138 + getRecord.executeContent.type === TaskTypeEnum.MODBUS_RTU
  139 + ? ' bg-blue-400'
  140 + : 'bg-green-400'
  141 + "
  142 + >
  143 + <CloudSyncOutlined class="svg:fill-light-50" />
  144 + </div>
  145 + <div class="text-gray-400 truncate">
  146 + <span>{{ TaskTypeNameEnum[getRecord.executeContent.type] }}</span>
  147 + <span class="mx-1">-</span>
  148 + <span>{{ TaskTargetNameEnum[getRecord.targetType] }}</span>
  149 + </div>
  150 + </div>
  151 + <div class="mt-4 flex justify-between items-center gap-3">
  152 + <Button size="small">
  153 + <div class="text-xs px-1">
  154 + <PlayCircleOutlined class="mr-1" />
  155 + <span>运行任务</span>
  156 + </div>
  157 + </Button>
  158 + <Tooltip title="最后运行时间:">
  159 + <div class="text-gray-400 text-xs truncate">
  160 + <span class="mr-2">间隔时间重复</span>
  161 + <span>三分钟前</span>
  162 + </div>
  163 + </Tooltip>
  164 + </div>
  165 +
  166 + <section
  167 + v-if="deviceTaskCardMode"
  168 + class="border-t mt-4 pt-2 text-sm border-gray-100 flex justify-between text-gray-400"
  169 + >
  170 + <div>
  171 + <span>允许该设备</span>
  172 + <Tooltip title="设置是否允许当前设备定时执行任务。该选项不影响手动执行任务。">
  173 + <QuestionCircleOutlined class="ml-1" />
  174 + </Tooltip>
  175 + </div>
  176 + <div>
  177 + <Switch
  178 + size="small"
  179 + :loading="loading"
  180 + :checked="getCancelState"
  181 + @click="handleCancelTask"
  182 + />
  183 + </div>
  184 + </section>
  185 + </Card>
  186 +</template>
... ...
  1 +import { FormSchema } from '/@/components/Form';
  2 +
  3 +export enum FormFieldsEnum {
  4 + DEVICE_TYPE = 'targetType',
  5 + TASK_TYPE = 'taskType',
  6 + TASK_STATUS = 'state',
  7 +}
  8 +
  9 +export enum TaskTargetEnum {
  10 + DEVICES = 'DEVICES',
  11 + PRODUCTS = 'PRODUCTS',
  12 + ALL = 'all',
  13 +}
  14 +
  15 +export enum TaskTargetNameEnum {
  16 + DEVICES = '设备',
  17 + PRODUCTS = '产品',
  18 + ALL = '不限任务目标类型',
  19 +}
  20 +
  21 +export enum TaskTypeEnum {
  22 + CUSTOM = 'CUSTOM',
  23 + MODBUS_RTU = 'MODBUS_RTU',
  24 +}
  25 +
  26 +export enum TaskTypeNameEnum {
  27 + MODBUS_RTU = 'MODBUS_RTU轮询',
  28 + CUSTOM = '自定义数据下发',
  29 +}
  30 +
  31 +export enum TaskStatusEnum {
  32 + DEACTIVATE,
  33 + NORMAL,
  34 +}
  35 +
  36 +export enum TaskStatusNameEnum {
  37 + DEACTIVATE = '已停用',
  38 + NORMAL = '正常',
  39 +}
  40 +
  41 +export const formSchemas: FormSchema[] = [
  42 + {
  43 + label: '目标类型',
  44 + component: 'Select',
  45 + field: FormFieldsEnum.DEVICE_TYPE,
  46 + componentProps: {
  47 + options: [
  48 + { label: TaskTargetNameEnum.DEVICES, value: TaskTargetEnum.DEVICES },
  49 + { label: TaskTargetNameEnum.PRODUCTS, value: TaskTargetEnum.PRODUCTS },
  50 + ],
  51 + placeholder: '请选择目标类型',
  52 + getPopupContainer: () => document.body,
  53 + },
  54 + },
  55 + // {
  56 + // label: '',
  57 + // component: 'Select',
  58 + // field: FormFieldsEnum.TASK_TYPE,
  59 + // defaultValue: TaskTypeEnum.ALL,
  60 + // componentProps: {
  61 + // options: [
  62 + // { label: TaskTypeNameEnum.CUSTOM, value: TaskTypeEnum.CUSTOM },
  63 + // { label: TaskTypeNameEnum.MODBUS_RTU, value: TaskTypeEnum.MODBUS_RTU },
  64 + // ],
  65 + // getPopupContainer: () => document.body,
  66 + // },
  67 + // },
  68 + {
  69 + label: '状态',
  70 + component: 'Select',
  71 + field: FormFieldsEnum.TASK_STATUS,
  72 + componentProps: {
  73 + options: [
  74 + { label: TaskStatusNameEnum.NORMAL, value: TaskStatusEnum.NORMAL },
  75 + { label: TaskStatusNameEnum.DEACTIVATE, value: TaskStatusEnum.DEACTIVATE },
  76 + ],
  77 + placeholder: '请选择状态',
  78 + getPopupContainer: () => document.body,
  79 + },
  80 + },
  81 +];
  82 +
  83 +export enum PermissionEnum {
  84 + DELETE = 'delete',
  85 +}
  86 +
  87 +export enum StateEnum {
  88 + CLOSE,
  89 + ENABLE,
  90 +}
... ...
  1 +<script setup lang="ts">
  2 + import { Button, List, Tooltip } from 'ant-design-vue';
  3 + import { PageWrapper } from '/@/components/Page';
  4 + import { BasicForm, useForm } from '/@/components/Form';
  5 + import { formSchemas } from './config';
  6 + import { TaskCard } from './components/TaskCard';
  7 + import { getTaskCenterList } from '/@/api/task';
  8 + import { ref } from 'vue';
  9 + import { onMounted } from 'vue';
  10 + import { DetailModal } from './components/DetailModal';
  11 + import { useModal } from '/@/components/Modal';
  12 + import { TaskRecordType } from '/@/api/task/model';
  13 + import { reactive } from 'vue';
  14 + import { ReloadOutlined } from '@ant-design/icons-vue';
  15 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  16 + import { ModalParamsType } from '/#/utils';
  17 +
  18 + const [registerModal, { openModal }] = useModal();
  19 +
  20 + const [registerForm, { getFieldsValue }] = useForm({
  21 + schemas: formSchemas,
  22 + baseColProps: { span: 8 },
  23 + compact: true,
  24 + showAdvancedButton: true,
  25 + labelWidth: 100,
  26 + submitFunc: async () => {
  27 + pagination.params = getFieldsValue();
  28 + getDataSource();
  29 + },
  30 + });
  31 +
  32 + const pagination = reactive({
  33 + total: 10,
  34 + current: 1,
  35 + showQuickJumper: true,
  36 + size: 'small',
  37 + showTotal: (total: number) => `共 ${total} 条数据`,
  38 + params: {} as Recordable,
  39 + });
  40 +
  41 + const dataSource = ref<TaskRecordType[]>([]);
  42 + const loading = ref(false);
  43 + const getDataSource = async () => {
  44 + try {
  45 + loading.value = true;
  46 + const { items } = await getTaskCenterList({ page: 1, pageSize: 10, ...pagination.params });
  47 + dataSource.value = items;
  48 + } catch (error) {
  49 + throw error;
  50 + } finally {
  51 + loading.value = false;
  52 + }
  53 + };
  54 +
  55 + const handleEdit = (record: TaskRecordType) => {
  56 + openModal(true, {
  57 + record,
  58 + mode: DataActionModeEnum.UPDATE,
  59 + } as ModalParamsType);
  60 + };
  61 +
  62 + const reload = () => getDataSource();
  63 +
  64 + onMounted(() => {
  65 + getDataSource();
  66 + });
  67 +</script>
  68 +
  69 +<template>
  70 + <PageWrapper class="container">
  71 + <section
  72 + class="bg-light-50 flex p-4 justify-between items-center x dark:text-gray-300 dark:bg-dark-900"
  73 + >
  74 + <div class="text-2xl">任务</div>
  75 + <Button
  76 + type="primary"
  77 + @click="openModal(true, { mode: DataActionModeEnum.CREATE } as ModalParamsType)"
  78 + >创建任务</Button
  79 + >
  80 + </section>
  81 + <section
  82 + class="form-container bg-light-50 px-4 pt-4 mt-4 x dark:text-gray-300 dark:bg-dark-900"
  83 + >
  84 + <BasicForm @register="registerForm" />
  85 + </section>
  86 + <section class="bg-light-50 mt-4 p-4 x dark:text-gray-300 dark:bg-dark-900">
  87 + <List
  88 + :dataSource="dataSource"
  89 + :pagination="pagination"
  90 + :grid="{ gutter: 16, xs: 1, sm: 1, md: 2, lg: 3, xl: 3, xxl: 4, column: 4 }"
  91 + :loading="loading"
  92 + >
  93 + <template #header>
  94 + <section class="flex justify-end gap-4">
  95 + <!-- <CardLayoutButton /> -->
  96 + <Tooltip title="刷新">
  97 + <Button type="primary" @click="getDataSource">
  98 + <ReloadOutlined :spin="loading" />
  99 + </Button>
  100 + </Tooltip>
  101 + </section>
  102 + </template>
  103 + <template #renderItem="{ item }">
  104 + <List.Item :key="item.id">
  105 + <TaskCard :record="item" :reload="reload" @edit="handleEdit" />
  106 + </List.Item>
  107 + </template>
  108 + </List>
  109 + </section>
  110 + <DetailModal @register="registerModal" :reload="reload" />
  111 + </PageWrapper>
  112 +</template>
  113 +
  114 +<style lang="less" scoped></style>
... ...
1 1 import type { ComputedRef, Ref } from 'vue';
  2 +import { DataActionModeEnum } from '/@/enums/toolEnum';
2 3
3 4 export type DynamicProps<T> = {
4 5 [P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>;
5 6 };
  7 +
  8 +export interface ModalParamsType<T = Recordable> {
  9 + mode: DataActionModeEnum;
  10 + record: T;
  11 +}
... ...