1
头图

本篇主要介绍formilyjs,它是阿里的统一前端表单解决方案,拥有比较完整的表单设计和复杂的场景处理功能,当然本篇主要浅显的介绍其表单设计及展示功能,将其应用到我们的流程可视化自定义编辑中,至于formilyjs自定义开发控件的话后续有机会单独篇章进行说明。

designable可视化表单设计

formilyjs官网提供了Formily 设计器,我们现在把这个表单设计器拉下来变成我们的一个页面:

新建src/playground目录,

yarn add @designable/formily-antd @ant-design/pro-components

设计器github,把playgroundsrc目录下相关文件拷贝下来放入playground/details中,调整一下引用结构(略过,见代码)。

// routes
{
  name: '表单模板',
  path: '/playground',
  icon: 'FormOutlined',
  component: '@/pages/playground',
},
{
  name: '表单设计',
  path: '/playground/details',
  component: '@/pages/playground/details',
  headerRender: false,
  footerRender: false,
  menuRender: false,
  hideInMenu: true,
},

表单模板列表

新建playground/index.tsxplayground/Preview.tsx,前者用作新建表单模板,后者为表单设计后的预览

// playground/index.tsx
import type { ActionType, ProColumns } from '@ant-design/pro-components';
import { ProTable } from '@ant-design/pro-components';
import { PageContainer } from '@ant-design/pro-layout';
import { FormItem, Input, FormDialog, FormLayout } from '@formily/antd';
import { createSchemaField } from '@formily/react';
import { Dropdown, Menu, Popconfirm, Space, Button } from 'antd';
import { FC, useRef, useState } from 'react';
import Preview from './Preview';
import { LgetItem, LsetItem } from '@/utils/storage';
import { uuidv4 } from '@antv/xflow';
import { history } from 'umi';

const SchemaField = createSchemaField({
  components: {
    FormItem,
    Input,
  },
});

const schema = {
  type: 'object',
  properties: {
    name: {
      type: 'string',
      title: '模板名',
      required: true,
      'x-decorator': 'FormItem',
      'x-component': 'Input',
    },
  },
};

const Playground: FC = () => {
  const [modalConfig, setModalConfig] = useState<{ [key: string]: any }>({});
  const previewRef = useRef<{ setVisible: (flag: boolean) => void }>(null);
  const actionRef = useRef<ActionType>();
  const columns: ProColumns<any>[] = [
    {
      dataIndex: 'name',
      title: '模板名',
    },
    {
      dataIndex: 'params',
      title: '是否配置',
      renderText(text, record, index, action) {
        return text ? '是' : '否';
      },
      search: false,
    },
    {
      title: '操作',
      valueType: 'option',
      render: (_, record) => {
        return [
          <a
            key="preview"
            onClick={() => {
              setModalConfig(record);
              previewRef.current?.setVisible(true);
            }}
          >
            预览
          </a>,
          <a key="edit" onClick={() => handleEdit(record)}>
            配置
          </a>,
          <a key="del" onClick={() => handleDel(record)}>
            删除
          </a>,
        ];
      },
    },
  ];

  const handleEdit = (record: any) => {
    history.push(`/playground/details?id=${record.id}`);
  };

  const handleDel = (record: any) => {
    const playgroundList = LgetItem('playgroundList') || [];
    LsetItem(
      'playgroundList',
      playgroundList.filter((s) => s.id !== record.id),
    );
    actionRef.current?.reload();
  };

  const handleAdd = () => {
    FormDialog('新增模板', () => {
      return (
        <FormLayout labelCol={6} wrapperCol={10}>
          <SchemaField schema={schema} />
        </FormLayout>
      );
    })
      .forOpen((payload, next) => {
        next({
          initialValues: {
            name: '',
          },
        });
      })
      .forConfirm((payload, next) => {
        const playgroundList = LgetItem('playgroundList') || [];
        playgroundList.push({
          name: payload.getFormState().values.name,
          id: uuidv4(),
        });
        LsetItem('playgroundList', playgroundList);
        actionRef.current?.reload();
        next(payload);
      })
      .forCancel((payload, next) => {
        next(payload);
      })
      .open()
      .then(console.log);
  };

  const getData = async (params: any) => {
    const data = LgetItem('playgroundList');
    return {
      data: data ?? [],
      success: true,
      total: data?.length ?? 0,
    };
  };

  return (
    <PageContainer>
      <ProTable
        columns={columns}
        actionRef={actionRef}
        request={async (params: any, _sorter: any, _filter: any) => {
          return await getData(params);
        }}
        rowKey="id"
        pagination={{
          showQuickJumper: true,
        }}
        toolBarRender={() => [
          <Button key="button" type="primary" onClick={handleAdd}>
            新增
          </Button>,
        ]}
      />
      <Preview previewRef={previewRef} modalConfig={modalConfig} />
    </PageContainer>
  );
};

export default Playground;
// playground/Preview.tsx
import { FC, Ref, useEffect, useImperativeHandle, useState } from 'react';
import { Modal } from 'antd';
import {
  FormItem,
  Input,
  Form,
  Submit,
  ArrayBase,
  ArrayCards,
  ArrayCollapse,
  ArrayItems,
  ArrayTable,
  ArrayTabs,
  BaseItem,
  Cascader,
  Checkbox,
  DatePicker,
  Editable,
  FormButtonGroup,
  FormCollapse,
  FormGrid,
  FormTab,
  GridColumn,
  NumberPicker,
  Password,
  PreviewText,
  Radio,
  Reset,
  Select,
  SelectTable,
  Space,
  Switch,
  TimePicker,
  Transfer,
  TreeSelect,
  Upload,
} from '@formily/antd';
import * as aaa from '@formily/antd';
import { createSchemaField } from '@formily/react';
import { LgetItem } from '@/utils/storage';
import { createForm } from '@formily/core';
console.log(aaa);
interface PreviewProps {
  previewRef: Ref<{ setVisible: (flag: boolean) => void }>;
  modalConfig: { [key: string]: any };
}

const SchemaField = createSchemaField({
  components: {
    Input,
    ArrayBase,
    ArrayCards,
    ArrayCollapse,
    ArrayItems,
    ArrayTable,
    ArrayTabs,
    BaseItem,
    Cascader,
    Checkbox,
    DatePicker,
    Editable,
    Form,
    FormButtonGroup,
    FormCollapse,
    FormGrid,
    FormItem,
    FormTab,
    GridColumn,
    NumberPicker,
    Password,
    PreviewText,
    Radio,
    Reset,
    Select,
    SelectTable,
    Space,
    Submit,
    Switch,
    TimePicker,
    Transfer,
    TreeSelect,
    Upload,
  },
});

const Preview: FC<PreviewProps> = ({ previewRef, modalConfig }) => {
  const [visible, setVisible] = useState(false);
  useImperativeHandle(previewRef, () => ({
    setVisible,
  }));

  const [params, setParams] = useState({});
  const normalForm = createForm({});
  useEffect(() => {
    if (modalConfig && visible) {
      const playgroundList = LgetItem('playgroundList') || [];
      const data = playgroundList.find((s) => s.id === modalConfig.id);
      setParams(data?.params || {});
    }
  }, [modalConfig, visible]);

  const handleCancel = () => {
    setVisible(false);
  };

  return (
    <Modal
      title="模板预览"
      visible={visible}
      onCancel={handleCancel}
      footer={null}
    >
      <Form form={normalForm} onAutoSubmit={console.log} {...params.form}>
        <SchemaField schema={params.schema} />
        <Submit block>保存</Submit>
      </Form>
    </Modal>
  );
};

export default Preview;

这里我们简单的把建立的表单数据存储在本地localStorage中,点击配置后跳转到我们的设计页面,
调整一下表单设计的保存,我使用设计器拖拽设计完成后,进行数据存储,找到对应id的表单模板将设计的schema赋值给params

// details/service/schema.ts
import { Engine } from '@designable/core';
import {
  transformToSchema,
  transformToTreeNode,
} from '@designable/formily-transformer';
import { message } from 'antd';
import { LgetItem, LsetItem } from '@/utils/storage';

function fixUrlHash(url: string) {
  let fixedUrl = new URL(url);
  let search = fixedUrl.search;
  let hash = fixedUrl.hash;
  const position = fixedUrl.hash.indexOf('?');
  if (search.length <= 1 && position >= 0) {
    search = hash.slice(position);
    hash = hash.slice(0, position);
    fixedUrl.hash = hash;
    fixedUrl.search = search;
    fixedUrl.href = fixedUrl.toString();
  }
  return fixedUrl;
}

export const saveSchema = (designer: Engine) => {
  const url = fixUrlHash(window.location.href);
  const searchParams = new URLSearchParams(url.search);
  let playgroundList = LgetItem('playgroundList') || [];
  playgroundList = playgroundList.map((s) => {
    if (s.id === searchParams.get('id')) {
      return {
        ...s,
        params: transformToSchema(designer.getCurrentTree()),
      };
    }
    return s;
  });
  LsetItem('playgroundList', playgroundList);
  message.success('保存成功');
};

export const loadInitialSchema = (designer: Engine) => {
  const url = fixUrlHash(window.location.href);
  const searchParams = new URLSearchParams(url.search);
  const playgroundList = LgetItem('playgroundList') || [];
  const data = playgroundList.find((s) => s.id === searchParams.get('id'));
  try {
    designer.setCurrentTree(transformToTreeNode(data?.params || []));
  } catch {}
};

模板设计

然后回到模板列表页,点击预览查看我们设计的表单是否正常显示

模板预览

下一章我将介绍表单设计器的一些高级配置,比如动态请求,封装接口注入,表单联动配置等,敬请期待。

本文地址:链接
本文github地址:链接
github demo地址:链接


陌路凡歌
7.9k 声望258 粉丝

人笨,多听,多写,多看书