BasicConfiguration.vue 10.8 KB
<script lang="ts" setup>
  import {
    CopyOutlined,
    DeleteOutlined,
    SettingOutlined,
    SwapOutlined,
  } from '@ant-design/icons-vue';
  import { Tooltip, Button, Alert } from 'ant-design-vue';
  import { FormActionType, useForm } from '/@/components/Form';
  import { basicSchema, DataSourceField } from '../config/basicConfiguration';
  import BasicForm from '/@/components/Form/src/BasicForm.vue';
  import { ref, shallowReactive, unref, nextTick, watch, computed, onMounted } from 'vue';
  import VisualOptionsModal from './VisualOptionsModal.vue';
  import { useModal } from '/@/components/Modal';
  import { buildUUID } from '/@/utils/uuid';
  import type { ComponentInfo, DataSource } from '/@/api/dataBoard/model';
  import { useMessage } from '/@/hooks/web/useMessage';
  import { DataBoardLayoutInfo } from '../../types/type';
  import { getDataSourceComponent } from './DataSourceForm/help';
  import { FrontComponent, FrontComponentCategory } from '../../const/const';
  import { isNullAndUnDef } from '/@/utils/is';
  import { useSortable } from '/@/hooks/web/useSortable';
  import { cloneDeep } from 'lodash-es';
  import { frontComponentMap } from '../../components/help';
  import { isControlComponent } from '../config/basicConfiguration';

  type DataSourceFormEL = { [key: string]: Nullable<FormActionType> };

  type DataSourceEl = DataSource & { id: string };

  const props = defineProps<{
    record: DataBoardLayoutInfo;
    frontId?: FrontComponent;
    defaultConfig?: Partial<ComponentInfo>;
    componentCategory?: FrontComponentCategory;
  }>();

  const { createMessage } = useMessage();

  const dataSource = ref<DataSourceEl[]>([
    { id: buildUUID(), componentInfo: props.defaultConfig || {} } as unknown as DataSourceEl,
  ]);

  const [basicRegister, basicMethod] = useForm({
    schemas: basicSchema,
    showActionButtonGroup: false,
    labelWidth: 96,
  });

  const dataSourceEl = shallowReactive<DataSourceFormEL>({} as unknown as DataSourceFormEL);

  const setFormEl = (el: any, id: string) => {
    if (id && el) {
      const { formActionType } = el as unknown as { formActionType: FormActionType };
      dataSourceEl[id] = formActionType;
    }
  };

  const validate = async () => {
    await basicMethod.validate();
    await validateDataSourceField();
  };

  const getAllDataSourceFieldValue = () => {
    const _dataSource = getDataSourceField();
    const basicInfo = basicMethod.getFieldsValue();
    return {
      ...basicInfo,
      dataSource: _dataSource,
    };
  };

  const validateDataSourceField = async () => {
    const hasExistEl = Object.keys(dataSourceEl).filter((key) => dataSourceEl[key]);
    const _dataSource: Record<DataSourceField, string>[] = [];
    for (const id of hasExistEl) {
      const flag = (await (dataSourceEl[id] as FormActionType).validate()) as Record<
        DataSourceField,
        string
      >;
      flag && _dataSource.push(flag);
    }

    if (
      [
        FrontComponent.MAP_COMPONENT_TRACK_HISTORY,
        FrontComponent.MAP_COMPONENT_TRACK_REAL,
      ].includes(props.frontId!)
    ) {
      await validateMapComponent(_dataSource);
    }
    return _dataSource;
  };

  const validateMapComponent = async (dataSource: Record<DataSourceField, string>[]) => {
    if (dataSource.length) {
      const firstRecord = dataSource.at(0)!;
      const { deviceId } = firstRecord;
      const flag = dataSource.every((item) => item.deviceId === deviceId);
      if (!flag) {
        createMessage.warning('地图组件绑定的数据源应该一致');
        return Promise.reject(false);
      }
    }
  };

  const getDataSourceField = () => {
    const hasExistEl = Object.keys(dataSourceEl).filter((key) => dataSourceEl[key]);
    const _dataSource: DataSource[] = [];

    for (const id of hasExistEl) {
      const index = unref(dataSource).findIndex((item) => item.id === id);
      const value = (dataSourceEl[id] as FormActionType).getFieldsValue() as DataSource;
      if (!~index) continue;
      const componentInfo = unref(dataSource)[index].componentInfo || {};
      _dataSource[index] = {
        ...value,
        componentInfo: { ...(props.defaultConfig || {}), ...componentInfo },
      };
    }

    return _dataSource;
  };

  const handleCopy = async (data: DataSourceEl) => {
    const value = (dataSourceEl[data.id] as FormActionType).getFieldsValue() as DataSource;
    const index = unref(dataSource).findIndex((item) => item.id === data.id);

    const componentInfo = ~index
      ? unref(dataSource)[index].componentInfo
      : ({} as unknown as ComponentInfo);

    const copyRecordId = buildUUID();
    unref(dataSource).push({
      ...value,
      id: copyRecordId,
      componentInfo,
    });
    await nextTick();
    (dataSourceEl[copyRecordId] as FormActionType).setFieldsValue(value);
    (dataSourceEl[copyRecordId] as FormActionType).clearValidate();
  };

  const [registerVisualOptionModal, { openModal }] = useModal();

  const handleSetting = (item: DataSourceEl) => {
    if (!props.frontId) {
      createMessage.warning('请先选择可视化组件');
      return;
    }

    const componentInfo: ComponentInfo = {
      ...(props.defaultConfig || {}),
      ...(item.componentInfo || {}),
    };

    openModal(true, {
      recordId: item.id,
      componentInfo,
    });
  };

  const handleDelete = (data: DataSourceEl) => {
    const index = unref(dataSource).findIndex((item) => item.id === data.id);
    ~index && unref(dataSource).splice(index, 1);
    dataSourceEl[data.id] = null;
  };

  const isMapComponent = computed(() => {
    return props.componentCategory === FrontComponentCategory.MAP;
  });

  const handleAdd = () => {
    if (unref(isMapComponent) && unref(dataSource).length === 2) {
      createMessage.warning('地图组件只能绑定两条数据源');
      return;
    }
    unref(dataSource).push({
      id: buildUUID(),
      componentInfo: props.defaultConfig || {},
    } as unknown as DataSourceEl);
  };

  const echoDataSource = () => {
    basicMethod.setFieldsValue(props.record.record);
    basicMethod.clearValidate();
    dataSource.value = [];
    dataSource.value = props.record.record.dataSource.map((item) => {
      const id = buildUUID();

      nextTick(() => {
        (dataSourceEl[id] as FormActionType).setFieldsValue(item);
        (dataSourceEl[id] as FormActionType).clearValidate();
      });
      return {
        id,
        ...item,
      };
    });
  };

  const showSettingButton = computed(() => {
    const flag = frontComponentMap.get(props.frontId!)?.hasSetting;
    return flag;
  });

  watch(
    () => props.record,
    () => {
      if (Object.keys(props.record).length) echoDataSource();
    }
  );

  const handleRowComponentInfo = (recordId: string, value: ComponentInfo) => {
    const index = unref(dataSource).findIndex((item) => item.id === recordId);
    ~index && (unref(dataSource)[index].componentInfo = value);
  };

  const dataSourceComponent = computed(() => {
    return getDataSourceComponent(props.frontId as FrontComponent);
  });

  let inited = false;
  const formListEl = ref<HTMLElement>();
  async function handleSort() {
    if (inited) return;
    await nextTick();
    const formList = unref(formListEl);
    if (!formList) return;

    const { initSortable } = useSortable(unref(formList), {
      handle: '.sort-icon',
      onEnd: (evt) => {
        const { oldIndex, newIndex } = evt;
        if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) {
          return;
        }

        const _dataSource = cloneDeep(unref(dataSource));

        if (oldIndex > newIndex) {
          _dataSource.splice(newIndex, 0, _dataSource[oldIndex]);
          _dataSource.splice(oldIndex + 1, 1);
        } else {
          _dataSource.splice(newIndex + 1, 0, _dataSource[oldIndex]);
          _dataSource.splice(oldIndex, 1);
        }

        dataSource.value = _dataSource;
      },
    });
    initSortable();
    inited = true;
  }

  const isControlCmp = computed(() => {
    return isControlComponent(props.frontId as FrontComponent);
  });

  onMounted(() => handleSort());

  defineExpose({
    getAllDataSourceFieldValue,
    validate,
  });
</script>

<template>
  <section>
    <h3 class="w-24 text-right pr-2 my-4">基础信息</h3>
    <div class="w-3/4">
      <BasicForm @register="basicRegister" class="w-full" />
    </div>
    <Alert type="info" show-icon v-if="isControlCmp">
      <template #description>
        <div>
          控制组件数据源为TCP产品,则其控制命令下发为TCP产品 物模型=>服务,且不具备状态显示功能.
        </div>
        <div>
          控制组件数据源为非TCP产品,则其控制命令下发为产品 物模型=>属性,且具备状态显示功能.
        </div>
      </template>
    </Alert>
    <Alert type="info" show-icon v-if="isMapComponent">
      <template #description>
        <div>
          地图组件,需绑定两个数据源,且数据源为同一设备。第一数据源为经度,第二数据源为纬度,否则地图组件不能正常显示。
        </div>
      </template>
    </Alert>
    <h3 class="w-24 flex-shrink-0 text-right pr-2 my-4">选择数据源</h3>

    <section ref="formListEl">
      <div v-for="item in dataSource" :data-id="item.id" :key="item.id" class="flex bg-light-50">
        <div class="w-24 text-right flex justify-end"> 选择设备 </div>
        <div class="pl-2 flex-auto">
          <component
            :frontId="$props.frontId"
            :is="dataSourceComponent"
            :ref="(el) => setFormEl(el, item.id)"
          />
        </div>
        <div class="flex justify-center gap-3 w-28">
          <Tooltip title="复制">
            <CopyOutlined @click="handleCopy(item)" class="cursor-pointer text-lg !leading-32px" />
          </Tooltip>
          <Tooltip title="设置">
            <SettingOutlined
              v-show="showSettingButton"
              @click="handleSetting(item)"
              class="cursor-pointer text-lg !leading-32px"
            />
          </Tooltip>
          <Tooltip title="拖拽排序">
            <SwapOutlined
              class="cursor-pointer text-lg !leading-32px svg:transform svg:rotate-90 sort-icon"
            />
          </Tooltip>
          <Tooltip title="删除">
            <DeleteOutlined
              @click="handleDelete(item)"
              class="cursor-pointer text-lg !leading-32px"
            />
          </Tooltip>
        </div>
      </div>
    </section>

    <div class="text-center">
      <Button type="primary" @click="handleAdd">添加数据源</Button>
    </div>
    <VisualOptionsModal
      :value="props.frontId"
      @close="handleRowComponentInfo"
      @register="registerVisualOptionModal"
    />
  </section>
</template>

<style scoped>
  .data-source-form:deep(.ant-row) {
    width: 100%;
  }

  .data-source-form:deep(.ant-form-item-control-input-content > div > div) {
    width: 100%;
  }
</style>