vue2/vue3自定义表单功能百度amis低代码渲染、编辑器二次开发

完成实现amis自定义表单功能,编辑器自定义签名按钮、签名更新触发事件、只展示自己需要的按钮功能、表单设计动态表单,json中form的name为表名,其他name为表字段,处理下划线命名转化为驼峰形式

动态创建数据库表、列,表单渲染、查询数据填充表单。总的来说vue3版本的支持性更高,版本内容更支持react转换的数据。

vue2,vue3版本联调后端进行动态生成,数据渲染、数据回显、必填项验证功能、是否可编辑功能、编辑器设计已完成

使用Ts功能使用了vue3和react插件,jsx功能受到影响,不能在jsx中使用自定义组件了,使用vue的h方法渲染

更多功能可根据源码实现的功能进行进一步拆解实现vue版本

vue3版本

安装amis渲染、编辑器,amis由于是react开发,所以需要安装react依赖包

    vue2需要下载amis包到public中,渲染需要用window.amisRequire("amis")
    <link rel="stylesheet"href="./lib/amis/sdk/sdk.css">
    <link rel="stylesheet"href="./lib/amis/sdk/helper.css">
    <link rel="stylesheet"href="./lib/amis/sdk/iconfont.css">
    <link rel="stylesheet"href="./lib/amis/lib/themes/cxd.css">
    <script src="./lib/amis/sdk/sdk.js"></script>

依赖包

 "@fortawesome/fontawesome-free": "^5.15.3",
    "amis": "6.7.0",
    "amis-core": "6.7.0",
    "amis-formula": "6.7.0",
    "amis-ui": "6.7.0",
    "amis-editor": "6.7.0",
    "amis-editor-core": "6.7.0",
    "copy-to-clipboard": "^3.2.0",
    "mobx": "4.15.7",
    "mobx-react": "6.3.1",
    "mobx-state-tree": "3.17.3",
    "react": "^16.14.0",
    "react-dom": "^16.14.0",
    "amis-theme-editor": "2.0.10",
    "amis-theme-editor-helper": "2.0.26",
    "veaury": "^2.3.18", //转换react代码为vue3版本
    "webworkify-webpack": "file:./libs/webworkify-webpack" //amis内有安装插件一直下不下来,超时

amis内有安装插件一直下不下来webworkify-webpack,超时,所以改成本地目录引用,package.json添加,文件存放public同级目录

 "overrides": {
    "webworkify-webpack": "file:./libs/webworkify-webpack"
  },

amis-editor编辑器

可以直接从amis源代码中查找,拿到示例编辑器的样式,改为自己的图标文件
编辑器css amis-editor-demo.scss 源码中获取,根据自己的要求更改css内容

  vue2使用:
 import { ReactInVue } from "vuera"; // 修改导入方式
 
  components: {
    AmisEditor: ReactInVue(Editor),
  }
  
  <AmisEditor
        :theme="'cxd'"
        :preview="preview"
        :isMobile="isMobile"
        :value="schema"
        :onChange="(schema) => handleSchemaChange(schema)"
        class="is-fixed"
        :amisEnv="amisEnv"
      />
<template>
  <div class="Editor-Demo">
    <div class="Editor-header">
      <div class="Editor-title">{{ title }}</div>
      <div class="Editor-view-mode-group-container">
        <div class="Editor-view-mode-group">
          <div :class="['Editor-view-mode-btn', 'editor-header-icon', !isMobile ? 'is-active' : '']" @click="setIsMobile(false)">
            <SvgIcon name="pc-preview" :icon-style="{ width: '20px', height: '20px' }" />
          </div>
          <div :class="['Editor-view-mode-btn', 'editor-header-icon', isMobile ? 'is-active' : '']" @click="setIsMobile(true)">
            <SvgIcon name="h5-preview" :icon-style="{ width: '20px', height: '20px' }" />
          </div>
        </div>
      </div>

      <div class="Editor-header-actions">
        <div :class="['header-action-btn', 'm-1', preview ? 'primary' : '']" @click="setPreview()">
          {{ preview ? '编辑' : '预览' }}
        </div>
        <div :class="['header-action-btn', 'm-1']" @click="submit()">保存</div>
      </div>
    </div>

    <div class="Editor-inner">
      <AmisEditor
        :theme="'cxd'"
        :preview="preview"
        :isMobile="isMobile"
        :value="schema"
        @change="onChange"
        class="is-fixed"
        :amisEnv="amisEnv"
      />
    </div>
  </div>
</template>

<script setup name="formDesign">
// 引入一些样式依赖
import 'amis-ui/lib/themes/default.css';
import 'amis-ui/lib/themes/cxd.css';
import 'amis-editor-core/lib/style.css';
import 'amis/lib/themes/cxd.css';
import 'amis/lib/helper.css';
import 'amis/sdk/iconfont.css';
import '@/styles/amis-editor-demo.scss';

import { ref, reactive, toRaw } from 'vue';
import { applyReactInVue } from 'veaury';
import { Editor } from 'amis-editor';
import SvgIcon from '@/components/SvgIcon/index.vue';
import initAmisJson from '@/assets/json/initDesign.json';

import '@/utils/amis/DisabledEditorPlugin'; // 用于隐藏一些不需要的Editor预置组件
import '@/utils/amis/customEditor';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';

import { CreateDbTable, AsyncDbTable, GetFromDetail, BindFromDetail } from '@/api/modules/template';

const AmisEditor = applyReactInVue(Editor); //使用编辑器
const route = useRoute(); // 此处不能在函数内部定义,需要定义在外部
const router = useRouter();

const title = ref('');
title.value = route.query.title;

// 定义响应式数据
const isMobile = ref(false);
const preview = ref(false);
const schema = ref(initAmisJson);
const amisEnv = reactive({
  notify: (type, msg) => {
    console.log(`[Notify] ${type}: ${msg}`);
  },
  alert: msg => {
    alert(msg);
  },
  copy: text => {
    navigator.clipboard.writeText(text);
    console.log('Copied:', text);
  }
});

// 定义一个响应式对象
const rawData = reactive({
  webJson: '',
  appJson: ''
});

// 使用 toRaw() 获取原始对象
const jsonObj = toRaw(rawData);

// 获取、展示设计的json表单
const _getFormDesign = () => {
  const id = route.query.id;
  GetFromDetail({ id }).then(res => {
    if (res.code === 200) {
      jsonObj.webJson = res.data.webJson;
      jsonObj.appJson = res.data.appJson;
      const isApp = route.query.platform == 'app';
      schema.value = (isApp ? JSON.parse(res.data.appJson) : JSON.parse(res.data.webJson)) || initAmisJson;
    } else {
      ElMessage({
        message: res.message,
        type: 'error'
      });
    }
  });
};

route.query.id && _getFormDesign();

// 绑定web或app版本的动态表单
function _BindFromDetail(tableId) {
  let params = {
    id: route.query.id,
    tableId: tableId,
    appJson: jsonObj.appJson,
    webJson: jsonObj.webJson
  };
  console.log(params, jsonObj);
  route.query.platform == 'app'
    ? (params.appJson = replaceSnakeKeys(JSON.stringify(schema.value)))
    : (params.webJson = replaceSnakeKeys(JSON.stringify(schema.value)));

  BindFromDetail(params).then(res => {
    if (res.code === 200) {
      ElMessage({
        message: '表单设计成功',
        type: 'success'
      });
      setTimeout(() => {
        router.push({
          path: '/workOrderTemplate'
        });
      }, 1000);
    } else {
      ElMessage({
        message: res.message,
        type: 'error'
      });
    }
  });
}
// 处理驼峰命名
function processCamelCase(str) {
  if (!str) return '';
  return String(str)
    .replace(/(?:^|_| +)(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
    .replace(/_(.)/g, (_, c) => c.toUpperCase());
}

// 定义方法
const setIsMobile = value => {
  cleanUpIframes();
  isMobile.value = value;
};

const setPreview = () => {
  preview.value = !preview.value;
};

function cleanUpIframes() {
  const iframes = document.querySelectorAll('iframe.ae-PreviewIFrame'); // 根据具体生成的 iframe class 或属性调整选择器
  iframes.forEach(iframe => {
    let _iframe = iframe.contentWindow;
    console.log(_iframe);
    if (iframe) {
      iframe.src = 'about:blank';
      try {
        _iframe.document.write('');
        _iframe.document.clear();
        _iframe.close();
      } catch (e) {
        console.log(e);
      }
      iframe.parentNode.removeChild(iframe);
    }
    try {
      window.CollectGarbage();
    } catch (e) {
      // todo
    }
    iframe = null;
  });
}

const onChange = newSchema => {
  schema.value = newSchema;
  console.log('Schema onChange:', schema.value);
};

const submit = () => {
  if (!schema.value || !schema.value.body?.length) {
    return ElMessage.warning('请先配置表单');
  }

  const { code, tableId, title, webJson, platform, appJson } = route.query;

  if (!code) {
    return ElMessage.warning('作业编号不能为空');
  }

  schema.value.name = code;
  schema.value.title = title;

  let names = extractNames(schema.value);
  console.log(names);

  let isExistJson = platform == 'app' ? appJson : webJson;

  if (tableId && !isExistJson) {
    _BindFromDetail(tableId);
    return;
  }

  _createDbTable(names, code);
};

// 提取表单中的所有字段名称和类型
function extractNames(obj, names = []) {
  if (Array.isArray(obj)) {
    obj.forEach(item => extractNames(item, names));
  } else if (typeof obj === 'object' && obj !== null) {
    if (obj.name && obj.type !== 'form') {
      if (!names.some(field => field.name === obj.name)) {
        names.push({
          name: obj.name,
          type: obj.type
        });
      }
    }

    if (obj.type == 'input-table') {
      return names;
    }

    if (obj.type === 'flex' && Array.isArray(obj.items)) {
      obj.items.forEach(item => extractNames(item, names));
      return names;
    }

    if (obj.type === 'container' && Array.isArray(obj.body)) {
      obj.body.forEach(item => extractNames(item, names));
      return names;
    }

    Object.values(obj).forEach(value => extractNames(value, names));
  }
  return names;
}

/**
 * 将 snake_case 字符串转换为 camelCase
 * @param str - 输入的下划线命名字符串
 * @returns 转换后的驼峰命名字符串
 */
function snakeToCamel(str) {
  return str.replace(/_([a-z])/g, (_, p1) => p1.toUpperCase());
}

/**
 * 替换 JSON 字符串中 name/value 的 snake_case 为 camelCase
 * @param jsonStr - 原始 JSON 字符串
 * @returns 替换后的 JSON 字符串
 */
function replaceSnakeKeys(jsonStr) {
  return jsonStr.replace(/"(name)"\s*:\s*"([^"]*_[^"]*)"/g, (_, key, val) => {
    return `"${key}": "${snakeToCamel(val)}"`;
  });
}

function _createDbTable(params, code) {
  // 新增数据库表创建请求
  const requestData = {
    className: processCamelCase(code),
    dbTableName: processCamelCase(code),
    dynamicColumns: params.map(item => ({
      PropertyName: processCamelCase(item.name),
      DbColumnName: processCamelCase(item.name),
      Length: ['textarea', 'ensign-custom', 'checkboxes'].includes(item.type) ? 255 : 80,
      PropertyType: '0', // 默认字符串类型
      IsNullable: true
    }))
  };

  requestData.dynamicColumns.unshift({
    PropertyName: 'Id',
    DbColumnName: 'Id',
    PropertyType: '1', // 改为数字类型
    IsPrimarykey: true,
    IsIdentity: true // 自增
  });

  CreateDbTable(requestData).then(res => {
    console.log(res);
    if (res.statusCode === 200) {
      _asyncDbTable(code, 'add', res.data);
    } else {
      ElMessage({
        message: res.message,
        type: 'error'
      });
    }
  });
}

function _asyncDbTable(code, type, tableId) {
  AsyncDbTable({
    dbTableName: processCamelCase(code)
  }).then(res => {
    if (res.statusCode === 200) {
      ElMessage({
        message: '表单操作成功',
        type: 'success'
      });

      if (type == 'add') {
        _BindFromDetail(tableId);
      }
    } else {
      ElMessage({
        message: res.message,
        type: 'error'
      });
    }
  });
}
</script>

<style lang="scss" scoped>
@import './index.scss';
</style>


编辑器展示自定义功能’@/utils/amis/customEditor’

import { registerEditorPlugin, EditorManager, getSchemaTpl } from 'amis-editor-core';
import '@/utils/amis/MyRenderer';
import { BasePlugin } from 'amis-editor-core';
import type { BaseEventContext } from 'amis-editor-core';
import { getEventControlConfig } from 'amis-editor/lib/renderer/event-control/helper';

export class MyRendererPlugin extends BasePlugin {
  static id = 'SignPlugin';

  rendererName = 'ensign-custom'; // 对应渲染器类型

  name = '添加签名';
  description = '添加签名按钮,用于用户签名';

  tags = ['表单项'];
  icon = 'fa fa-user';

  scaffold = {
    type: 'ensign-custom',
    name: 'ensign',
    label: '添加签名'
  };

  previewSchema = {
    type: 'ensign-custom',
    name: 'ensign',
    label: '添加签名'
  };

  events = [
    {
      eventName: 'change',
      eventLabel: '签名更改',
      description: '修改签名内容时触发',
      dataSchema: (manager: EditorManager) => {
        const node = manager.store.getNodeById(manager.store.activeId);
        const schemas = manager.dataSchema.current.schemas;
        const dataSchema = schemas.find(item => item.properties?.[node!.schema.name]);

        return [
          {
            type: 'object',
            properties: {
              data: {
                type: 'object',
                title: '数据',
                properties: {
                  value: {
                    type: 'string',
                    ...((dataSchema?.properties?.[node!.schema.name] as any) ?? {}),
                    title: '添加签名'
                  }
                }
              }
            }
          }
        ];
      }
    }
  ];

  panelBodyCreator = (context: BaseEventContext) => {
    console.log(this.manager);
    return getSchemaTpl('tabs', [
      {
        title: '事件',
        className: 'p-none',
        body: [
          getSchemaTpl('eventControl', {
            name: 'onEvent',
            ...getEventControlConfig(this.manager, context)
          })
        ]
      }
    ]);
  };
}

registerEditorPlugin(MyRendererPlugin);

编辑器功能隐藏展示’@/utils/amis/DisabledEditorPlugin’

import { registerEditorPlugin, BasePlugin } from 'amis-editor';
import { RendererEventContext, SubRendererInfo, BasicSubRenderInfo } from 'amis-editor';

/**
 * 用于隐藏一些不需要的Editor组件
 * 备注: 如果不知道当前Editor中有哪些预置组件,可以在这里设置一个断点,console.log 看一下 renderers。
 */

// 允许在组件面板中显示的组件名称
const enabledRenderers = [
  'flex', // Flex布局
  'grid', // 分栏
  'container', // 折叠面板
  'collapse-group', // 容器
  'input-table', //表格编辑框
  'table-view', //表格视图
  'input-text', // 文本框
  'textarea', // 多行文本框
  'input-number', // 数字框
  'select', // 下拉框
  'checkboxes', // 复选框
  'radios', // 单选框
  'checkbox', // 勾选框
  'input-date', // 日期
  'input-date-range', // 日期范围
  'ensign-custom' // 添加签名
];

export class ManagerEditorPlugin extends BasePlugin {
  buildSubRenderers(
    context: RendererEventContext,
    renderers: Array<SubRendererInfo>
  ): BasicSubRenderInfo | Array<BasicSubRenderInfo> | void {
    // 更新NPM自定义组件排序和分类

    for (let index = 0, size = renderers.length; index < size; index++) {
      // 判断是否需要隐藏 Editor预置组件
      const pluginRendererName = renderers[index].rendererName || '';

      let disabledRendererPlugin = !enabledRenderers.includes(pluginRendererName);
      let disabledTagsPlugin = !renderers[index].tags?.includes('展示');
      renderers[index].disabledRendererPlugin = disabledTagsPlugin && disabledRendererPlugin;
    }
  }
}

registerEditorPlugin(ManagerEditorPlugin);

自定义按钮功能’@/utils/amis/MyRenderer’

插件和插件内图标需要挂载进新的vue实例中,这里不能使用Renderer,因为自定义表单属性,后续渲染器获取表单填写数据时使用的方法需要使用OptionsControl或者FormItem

import React from 'react';
import { OptionsControl, ScopedContext } from 'amis';
import { createApp } from 'vue';
import MyVue3Component from '@/components/CustomEsignature/index.vue';
import ElementPlus from 'element-plus';
import * as ElementPlusIconsVue from '@element-plus/icons-vue';

import { resolveEventData } from 'amis-core';

export default class MyRenderer extends React.Component<any> {
  private vueAppRef: any = null;
  static contextType = ScopedContext;

  // 获取DOM引用
  private domRef = React.createRef<HTMLDivElement>();

  componentDidMount() {
    // 在组件挂载时创建 Vue 应用
    console.log(this.props);
    const scoped: any = this.context;
    scoped.registerComponent(this);

    if (this.domRef.current) {
      this.vueAppRef = createApp(MyVue3Component, {
        signatureImgUrl: this.props.value, // 将 props 传递给 Vue 组件
        disabled: this.props.disabled, // 将 props 传递给 Vue 组件
        onUpdate: (newValue: string) => {
          // Vue 组件向外暴露数据,调用 onChange 方法
          if (this.props.onChange) {
            this.props.onChange(newValue);
          }

          // 自定义插件event中事件监听值变化,要使用这里触发
          this.props.dispatchEvent?.(
            'change',
            resolveEventData(this.props, {
              value: newValue,
              [this.props.name]: newValue
            })
          );
        }
      });

      for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
        this.vueAppRef.component(key, component);
      }

      this.vueAppRef.use(ElementPlus).mount(this.domRef.current);
      console.log('Vue App 已挂载');
    }
  }

  componentWillUnmount() {
    // 在组件卸载时销毁 Vue 应用实例
    const scoped: any = this.context;
    scoped.unRegisterComponent(this);

    if (this.vueAppRef) {
      this.vueAppRef.unmount();
      console.log('Vue App 已卸载');
    }
  }

  render() {
    // 渲染 Vue 应用容器
    return React.createElement('div', { ref: this.domRef });
  }
}

// 注册渲染器
OptionsControl({
  type: 'ensign-custom' // 渲染器类型
})(MyRenderer);

表单设计后json渲染页面 lowcode-engine

渲染页面直接在页面中引用组件传递组件参数即可,此处进行数据渲染、表单是否开启编辑、判断必填项进行提示、input-table这种无限新增的使用json文件上传进行保存

父组件使用:
<lowcode-engine
    ref="lowcodeRef"
    :form-json="formJson"
    :form-data="formData"
    :drawer-props="drawerPropsCopy"
    @close="handleDialogClosed"
    v-loading="loading"
  />
<template>
  <amisComponent ref="amisComponentRef" :form-json="formJsonCopy" :form-data="drawerPropsCopy.rowData"></amisComponent>
</template>

<script setup lang="ts">
import { ref, nextTick, watch } from 'vue';
import amisComponent from '@/components/AmisComponent/index.vue';
import { ElMessage } from 'element-plus';
import { CreateEntity, UpdateEntity, UploadFileJson, SaveWorkOrderTemplateTable } from '@/api/modules/template';

const props = defineProps({
  formJson: {
    type: Object,
    default: () => ({})
  },
  formData: {
    type: Object,
    default: () => ({})
  },
  drawerProps: {
    type: Object,
    default: () => ({
      isView: false,
      type: 'create',
      title: '',
      rowData: {}
    })
  }
});

const drawerPropsCopy = ref<any>(props.drawerProps);
let commandFormData = ref(props.formData);
const formJsonCopy = ref(props.formJson);

const amisComponentRef = ref();

const emit = defineEmits(['close']);

watch(
  () => props.formJson,
  newVal => {
    formJsonCopy.value = newVal;
  },
  { deep: true }
);

watch(
  () => props.drawerProps,
  newVal => {
    drawerPropsCopy.value = newVal;
  },
  { deep: true }
);

// 监听data数据变化,修改表单数据渲染、禁止编辑状态
watch(
  () => props.formData,
  newVal => {
    commandFormData.value = newVal;
    if (newVal) {
      getDetail();
      nextTick(() => {
        amisComponentRef.value && amisComponentRef.value.onRendering(formJsonCopy.value);
      });
    }
  },
  { deep: true }
);

const closePreviewDialog = () => {
  console.log('closePreviewDialog', formJsonCopy.value);
};

const getDetail = async () => {
  if (drawerPropsCopy.value.isView) {
    formJsonCopy.value.disabled = true;
    disableTableViewNames(formJsonCopy.value);
  }
  formJsonCopy.value.data = commandFormData.value || {};
};

getDetail();

// 禁止表单功能编辑
function disableTableViewNames(obj: any) {
  if (Array.isArray(obj)) {
    obj.forEach(item => disableTableViewNames(item));
  } else if (typeof obj === 'object' && obj !== null) {
    if (obj.type === 'table-view' && Array.isArray(obj.trs)) {
      obj.trs.forEach((tr: any) => {
        if (Array.isArray(tr.tds)) {
          tr.tds.forEach((td: any) => {
            if (Array.isArray(td.body)) {
              td.body.forEach((item: any) => disableTableViewNames(item));
            } else if (typeof td.body === 'object' && td.body !== null) {
              disableTableViewNames(td.body);
            }
          });
        }
      });
    }
    if (obj.type === 'input-table' && Array.isArray(obj.columns)) {
      obj.columns.forEach((column: any) => {
        column.disabled = true;
      });
    }
    if (obj.type === 'flex' && Array.isArray(obj.items)) {
      obj.items.forEach((item: any) => disableTableViewNames(item));
    }
    if (obj.type === 'container' && Array.isArray(obj.body)) {
      obj.body.forEach((item: any) => disableTableViewNames(item));
    }
    if (obj.name) {
      obj.disabled = true;
    }
    Object.values(obj).forEach(value => disableTableViewNames(value));
  }
}

// 提取表单中的所有必填字段名称和标签
function extractNames(obj: any, requiredFields: { name: string; label: string }[] = []) {
  if (Array.isArray(obj)) {
    obj.forEach(item => extractNames(item, requiredFields));
  } else if (obj && typeof obj === 'object') {
    if (obj.name && obj.required && obj.type !== 'form') {
      // 检查是否已经存在同名字段
      if (!requiredFields.some(field => field.name === obj.name)) {
        requiredFields.push({
          name: obj.name,
          label: obj.label || `未命名字段(${obj.name})`
        });
      }
    }
    if (obj.type === 'input-table') return requiredFields;
    if (obj.type === 'flex' && Array.isArray(obj.items)) {
      obj.items.forEach((item: any) => extractNames(item, requiredFields));
      return requiredFields;
    }
    if (obj.type === 'container' && Array.isArray(obj.body)) {
      obj.body.forEach((item: any) => extractNames(item, requiredFields));
      return requiredFields;
    }
    Object.values(obj).forEach(value => extractNames(value, requiredFields));
  }
  return requiredFields;
}

// 获取所有 input-table 类型的 name
function extractInputTableNames(obj: any, inputTableNames: string[] = []) {
  if (Array.isArray(obj)) {
    obj.forEach(item => extractInputTableNames(item, inputTableNames));
  } else if (obj && typeof obj === 'object') {
    if (obj.type === 'input-table' && obj.name) {
      if (!inputTableNames.some((field: string) => field === obj.name)) {
        inputTableNames.push(obj.name);
      }
    }

    // 处理 flex 类型
    if (obj.type === 'flex' && Array.isArray(obj.items)) {
      obj.items.forEach((item: any) => extractInputTableNames(item, inputTableNames));
    }

    // 处理 container 类型
    if (obj.type === 'container' && Array.isArray(obj.body)) {
      obj.body.forEach((item: any) => extractInputTableNames(item, inputTableNames));
    }

    Object.values(obj).forEach(value => extractInputTableNames(value, inputTableNames));
  }
  return inputTableNames;
}

// 处理为驼峰命名
function processCamelCase(str: string) {
  return String(str)
    .replace(/(?:^|_| +)(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
    .replace(/_(.)/g, (_, c) => c.toUpperCase());
}

function getFormData() {
  return amisComponentRef.value.submit();
}

const handleSubmit = (tableId?: number, isDraft?: boolean) => {
  const value = getFormData();
  console.log('提交值:', value);

  if (!tableId) {
    ElMessage.error('未查询到该动态表!');
    return;
  }

  if (!drawerPropsCopy.value.isView) {
    const requiredFields = extractNames(formJsonCopy.value, []);
    const missingLabels = requiredFields
      .filter(({ name }) => {
        const val = value[name];
        return (
          val === undefined ||
          val === null ||
          (typeof val === 'string' && !val.trim()) ||
          (Array.isArray(val) && val.length === 0)
        );
      })
      .map(field => field.label);

    if (missingLabels.length > 0) {
      ElMessage.warning(`以下必填项不能为空:${missingLabels.join(',')}`);
      return;
    }
  }

  // 检查是否有 input-table 类型的字段需要上传 JSON 文件
  const inputTableNames = extractInputTableNames(formJsonCopy.value);
  const promiseList: any = [];
  inputTableNames.forEach(async name => {
    if (value[name] && Array.isArray(value[name]) && value[name].length > 0) {
      promiseList.push(uploadJsonFile(name, value[name]));
    } else {
      value[name] = '';
    }
  });
  Promise.all(promiseList)
    .then(res => {
      res?.forEach(({ name, url }) => {
        value[name] = url;
      });
      drawerPropsCopy.value.type === 'create' ? _createEntity(tableId, value, isDraft) : _updateEntity(tableId, value);
    })
    .catch(error => {
      ElMessage.error(error);
    });
};

// 上传 JSON 文件
function uploadJsonFile(name: string, jsonData: any) {
  return new Promise(async (resolve, reject) => {
    const blob = new Blob([JSON.stringify(jsonData)], {
      type: 'application/json'
    });
    const formData = new FormData();
    formData.append('file', blob);
    formData.append('fileName', `${drawerPropsCopy.value.rowData.code}_${name}.json`);

    try {
      const res: any = await UploadFileJson(formData);
      if (res.code === 200) {
        resolve({ name, url: res.data });
      } else {
        reject(res.message || '文件上传失败');
      }
    } catch (error: any) {
      ElMessage.error(error.message);
      reject(error.message);
    }
  });
}

// 动态表实例生成后绑定业务id
function _bindTemplateTable(tableDataId: string, isDraft?: boolean) {
  SaveWorkOrderTemplateTable({
    ...drawerPropsCopy.value.rowData,
    isSubmit: isDraft ? false : true,
    tableDataId: tableDataId
  }).then(response => {
    if (response.code === 200) {
      ElMessage.success('操作成功');
      setTimeout(() => {
        emit('close');
      }, 1000);
    } else {
      ElMessage.error(response.message);
    }
  });
}

function _createEntity(tableId: number, value: any, isDraft: boolean = false) {
  const processData: Record<string, any> = {};
  for (const [key, val] of Object.entries(value)) {
    const camelKey = processCamelCase(key);
    processData[camelKey] = typeof val === 'object' || Array.isArray(val) ? JSON.stringify(val) : val;
  }
  const params = {
    tableId,
    dynamicData: processData
  };
  CreateEntity(params).then((res: any) => {
    if (res.statusCode === 200) {
      ElMessage.success('创建成功!');
      _bindTemplateTable(res.data, isDraft);
    } else {
      ElMessage.error(res.message);
    }
  });
}

function _updateEntity(tableId: number, value: any) {
  const processData: Record<string, any> = {};
  for (const [key, val] of Object.entries(value)) {
    const camelKey = processCamelCase(key);
    if (!val) continue;

    processData[camelKey] = typeof val === 'object' || Array.isArray(val) ? JSON.stringify(val) : val;
  }
  if (commandFormData.value.id) {
    processData['Id'] = commandFormData.value.id;
  }
  const params = {
    tableId,
    dynamicData: processData
  };
  UpdateEntity(params).then((res: any) => {
    if (res.statusCode === 200) {
      ElMessage.success('更新数据成功!');
      _bindTemplateTable(commandFormData.value.id);
    } else {
      ElMessage.error(res.message);
    }
  });
}

defineExpose({
  handleSubmit,
  closePreviewDialog
});
</script>


amisComponent组件

处理amis插件,进行json渲染表单、amis操作获取表单所填内容

<template>
  <div id="main-low-code">
    <div class="drawer-title">{{ formData.name }}</div>
    <div id="content-low-code" @click="cleanOverlay" style="max-height: 60vh; padding: 12px 24px 36px; overflow: auto"></div>
  </div>
</template>
<script setup lang="ts">
import { watch, nextTick, ref, onUnmounted } from 'vue';
// import { ElMessage } from 'element-plus';
import 'amis/sdk/sdk.js';
import 'amis/lib/themes/default.css';
import 'element-plus/dist/index.css'; // 引入 Element Plus 样式
import '@/utils/amis/customRenderer'; // 加载自定义渲染器
import '@/styles/amis-editor-demo.scss';

// import axios from 'axios';
// import copy from 'copy-to-clipboard';

const props = defineProps({
  formData: {
    type: Object,
    default: () => {
      return {};
    }
  },
  formJson: {
    type: Object,
    default: () => {
      return {};
    }
  }
});
// @ts-expect-error amis引用
const amis = amisRequire('amis/embed');
const amisScoped = ref();

watch(
  () => props.formJson,
  () => {
    nextTick(() => {
      onRendering(props.formJson);
    });
  },
  { immediate: true, deep: true }
);

function cleanOverlay(e: MouseEvent) {
  // 判断点击目标是否为弹窗元素
  const isPopOver = (e.target as HTMLElement).closest('.cxd-PopOver');
  if (isPopOver) return;

  document.querySelectorAll('.cxd-PopOver .cxd-PopOver-overlay').forEach(overlay => {
    if (overlay instanceof HTMLElement && overlay.click) {
      overlay.click();
    }
  });
}

function onRendering(data: any) {
  amisScoped.value = amis.embed('#content-low-code', data);
}

const submit = () => {
  let text = amisScoped.value.getComponentByName(props.formData.code).getValues();
  return text;
};

onUnmounted(() => {
  amisScoped.value.unmount();
});

defineExpose({
  submit,
  onRendering
});
</script>
<style lang="scss" scoped>
.drawer-title {
  margin-bottom: 12px;
  font-size: 18px;
  font-weight: bold;
  color: #333333;
  text-align: center;
}
</style>


自定义按钮功能渲染’@/utils/amis/customRenderer’

由于渲染页面使用的amisRequire(‘amis/embed’)来渲染的功能,所以不能用之前的MyRenderer,更改为import引用的amis如果可以使用也不用新的

新的也要
let amisLib = amisRequire('amis');  
然后amisLib.OptionsControl({
  type: 'ensign-custom' // 渲染器类型
})(CustomComponent);

Vue2 APP版本

app版本不能使用编辑器功能,提供展示动态表单渲染、数据回显、编辑表单数据

接入依赖包

 "amis-core": "6.7.0",
    "amis-formula": "6.7.0",
    "amis-ui": "6.7.0",
    "react": "^16.14.0",
    "react-dom": "^16.14.0"

util js 自定义组件

import Vue from "vue";
import MyVue2Component from "@/components/Esignature/index.vue";
import { resolveEventData } from "amis-core";


const amisLib = window.amisRequire ? window.amisRequire("amis") : null;
let React = window.amisRequire ? window.amisRequire("react") : null;

function CustomComponent(props) {
  let dom = React.useRef(null);
  let vueInstance = React.useRef(null);

  React.useEffect(() => {
    console.log("vue2props", props);

    if (dom.current) {
      // 创建 Vue 2 实例
      vueInstance.current = new Vue({
        el: dom.current,
        render: (h) =>
          h(MyVue2Component, {
            props: {
              signatureImgUrl: props.value,
              disabled: props.disabled,
            },
            on: {
              update: (newValue) => {
                if (props.onChange) {
                  props.onChange(newValue);
                }
                this.props.dispatchEvent?.(
		            'change',
		            resolveEventData(this.props, {
		              value: newValue,
		              [this.props.name]: newValue
		            })
          		);
              },
            },
          }),
      });

      vueInstance.current.$mount(dom.current);
      console.log("Vue 2 App 已挂载");
    }

    return () => {
      if (vueInstance.current) {
        vueInstance.current.$destroy();
        console.log("Vue 2 App 已卸载");
      }
    };
  }, []);

  return React.createElement("div", { ref: dom });
}
// 注册自定义组件
amisLib.OptionsControl({
  type: "ensign-custom", // 渲染器类型
})(CustomComponent);

页面渲染数据

  <lowcode-engine
        v-else
        ref="lowcodeRef"
        :form-json="formJson"
        :formData="formData"
        :drawerProps="drawerProps"
        @success="onClickLeft"
      />
 // 获取所有 input-table 类型的 name
    extractInputTableNames(obj, inputTableNames = []) {
      if (Array.isArray(obj)) {
        obj.forEach((item) =>
          this.extractInputTableNames(item, inputTableNames)
        );
      } else if (obj && typeof obj === "object") {
        if (
          obj.type === "input-table" &&
          obj.name &&
          !inputTableNames.some((field) => field === obj.name)
        ) {
          inputTableNames.push(obj.name);
        }

        // 处理 flex 类型
        if (obj.type === "flex" && Array.isArray(obj.items)) {
          obj.items.forEach((item) =>
            this.extractInputTableNames(item, inputTableNames)
          );
          return inputTableNames;
        }

        // 处理 container 类型
        if (obj.type === "container" && Array.isArray(obj.body)) {
          obj.body.forEach((item) =>
            this.extractInputTableNames(item, inputTableNames)
          );
          return inputTableNames;
        }

        Object.values(obj).forEach((value) =>
          this.extractInputTableNames(value, inputTableNames)
        );
      }
      return inputTableNames;
    },
    async loadData() {
      this.loading = true;
      try {
        const res = await QueryItem({
          tableId: this.$route.query.tableId,
          //查询条件
          dynamicData: {
            Id: this.currentId,
          },
        });

        const inputTableNames = this.extractInputTableNames(this.formJson);
        const promiseList = [];

        // 新增JSON字符串转换处理
        const convertedData = {};
        for (const [key, value] of Object.entries(res.data)) {
          let asyncFunc = new Promise(async (resolve) => {
            try {
              if (
                typeof value === "string" &&
                inputTableNames.includes(key) &&
                value.endsWith(".json")
              ) {
                // 如果是.ldsw.json文件路径,读取文件内容
                const response = await fetch(this.baseUrl + value);
                const fileContent = await response.text();
                convertedData[key] = JSON.parse(fileContent);
              } else {
                convertedData[key] =
                  typeof value === "string" ? JSON.parse(value) : value;
              }
              resolve();
            } catch (e) {
              convertedData[key] = value;
              resolve();
            }
          });

          promiseList.push(asyncFunc);
        }

        Promise.all(promiseList).then(() => {
          this.$set(this, "formData", convertedData);
        });
        this.loading = false;
      } catch (error) {
        console.error("数据加载失败", error);
        this.loading = false;
      }
    },

保存数据、渲染数据 和vue3web版本类似 lowcode-engine组件

<template>
  <amisComponent
    ref="amisComponentRef"
    :formJson="formJson"
    :formData="drawerProps.rowData || {}"
  ></amisComponent>
</template>

<script>
import amisComponent from "@/components/AmisComponent/index.vue";
import { CreateEntity, UpdateEntity, UploadFileJson } from "@/api/list";
import * as apiModule from "@/api/list";

export default {
  name: "LowcodeEngine",
  components: {
    amisComponent,
  },
  props: {
    formJson: {
      type: Object,
      default: () => ({}),
    },
    formData: {
      type: Object,
      default: () => ({}),
    },
    drawerProps: {
      type: Object,
      default: () => ({
        isView: false,
        type: "create",
        rowData: {},
      }),
    },
  },
  data() {
    return {
      isTk: false,
    };
  },
  created() {
    this.getDetail();
  },
  watch: {
    formData: {
      handler(newVal) {
        if (newVal) {
          this.getDetail();

          this.$nextTick(() => {
            this.$refs.amisComponentRef &&
              this.$refs.amisComponentRef.onRendering(this.formJson);
          });
        }
      },
      deep: true,
    },
  },
  methods: {
    getDetail() {
      if (this.drawerProps.isView) {
        this.formJson.disabled = true;
        this.disableTableViewNames(this.formJson);
      }
      this.formJson.data = this.formData;
    },
    disableTableViewNames(obj) {
      if (Array.isArray(obj)) {
        obj.forEach((item) => this.disableTableViewNames(item));
      } else if (typeof obj === "object" && obj !== null) {
        // 遍历 table-view
        if (obj.type === "table-view" && Array.isArray(obj.trs)) {
          obj.trs.forEach((tr) => {
            if (Array.isArray(tr.tds)) {
              tr.tds.forEach((td) => {
                if (Array.isArray(td.body)) {
                  td.body.forEach((item) => this.disableTableViewNames(item));
                } else if (typeof td.body === "object" && td.body !== null) {
                  this.disableTableViewNames(td.body);
                }
              });
            }
          });
        }

        if (obj.type === "input-table" && Array.isArray(obj.columns)) {
          obj.columns.forEach((columns) => {
            columns.disabled = true;
          });
        }

        // 遍历 flex.items -> container -> body
        if (obj.type === "flex" && Array.isArray(obj.items)) {
          obj.items.forEach((item) => this.disableTableViewNames(item));
        }

        if (obj.type === "container" && Array.isArray(obj.body)) {
          obj.body.forEach((item) => this.disableTableViewNames(item));
        }

        // 设置 disabled
        if (obj.name) {
          obj.disabled = true;
        }

        // 递归其他字段
        Object.values(obj).forEach((value) =>
          this.disableTableViewNames(value)
        );
      }
    },

    extractNames(obj, requiredFields = []) {
      if (Array.isArray(obj)) {
        obj.forEach((item) => this.extractNames(item, requiredFields));
      } else if (obj && typeof obj === "object") {
        if (obj.name && obj.required && obj.type !== "form") {
          if (!requiredFields.some((field) => field.name === obj.name)) {
            requiredFields.push({
              name: obj.name,
              label: obj.label || `未命名字段(${obj.name})`,
            });
          }
        }
        if (obj.type === "input-table") {
          return requiredFields;
        }

        if (obj.type === "flex" && Array.isArray(obj.items)) {
          obj.items.forEach((item) => this.extractNames(item, requiredFields));
          return requiredFields;
        }

        if (obj.type === "container" && Array.isArray(obj.body)) {
          obj.body.forEach((item) => this.extractNames(item, requiredFields));
          return requiredFields;
        }

        Object.values(obj).forEach((value) =>
          this.extractNames(value, requiredFields)
        );
      }
      return requiredFields;
    },
    // 获取所有 input-table 类型的 name
    extractInputTableNames(obj, inputTableNames = []) {
      if (Array.isArray(obj)) {
        obj.forEach((item) =>
          this.extractInputTableNames(item, inputTableNames)
        );
      } else if (obj && typeof obj === "object") {
        if (
          obj.type === "input-table" &&
          obj.name &&
          !inputTableNames.some((field) => field === obj.name)
        ) {
          inputTableNames.push(obj.name);
        }

        // 处理 flex 类型
        if (obj.type === "flex" && Array.isArray(obj.items)) {
          obj.items.forEach((item) =>
            this.extractInputTableNames(item, inputTableNames)
          );
          return inputTableNames;
        }

        // 处理 container 类型
        if (obj.type === "container" && Array.isArray(obj.body)) {
          obj.body.forEach((item) =>
            this.extractInputTableNames(item, inputTableNames)
          );
          return inputTableNames;
        }

        Object.values(obj).forEach((value) =>
          this.extractInputTableNames(value, inputTableNames)
        );
      }
      return inputTableNames;
    },

    processCamelCase(str) {
      if (!str) return "";
      return String(str)
        .replace(/(?:^|_| +)(\w)/g, (_, c) => (c ? c.toUpperCase() : ""))
        .replace(/_(.)/g, (_, c) => c.toUpperCase());
    },
    getFormData() {
      return this.$refs.amisComponentRef.submit();
    },
    // 上传 JSON 文件
    async uploadJsonFile(name, jsonData) {
      return new Promise(async (resolve, reject) => {
        const blob = new Blob([JSON.stringify(jsonData)], {
          type: "application/json",
        });
        const formData = new FormData();
        formData.append(
          "InputImageFile",
          blob,
          `${this.drawerProps.rowData.code}_${name}.json`
        );

        try {
          const res = await UploadFileJson(formData);
          if (res.code === 200) {
            resolve({ name, url: res.data });
          } else {
            reject(res.msg || "文件上传失败");
          }
        } catch (error) {
          this.$toast(error.message);
          reject(error.message);
        }
      });
    },
    handleSubmit(tableId, isTK = false) {
      if (isTK) this.isTk = true;
      let value = this.getFormData();
      console.log(value);
      if (!tableId) {
        this.$toast(`未查询到该动态表!`);
        return;
      }

      if (!this.drawerProps.isView) {
        const requiredFields = this.extractNames(this.formJson, []);
        const missingLabels = requiredFields
          .filter(({ name }) => {
            const val = value[name];
            return (
              val === undefined ||
              val === null ||
              (typeof val === "string" && !val.trim()) ||
              (Array.isArray(val) && val.length === 0)
            );
          })
          .map((field) => field.label);
        if (missingLabels.length > 0) {
          this.$toast(`以下必填项不能为空:${missingLabels.join(",")}`);
          return;
        }
      }

      const inputTableNames = this.extractInputTableNames(this.formJson);
      const promiseList = [];

      inputTableNames.forEach(async (name) => {
        if (
          value[name] &&
          Array.isArray(value[name]) &&
          value[name].length > 0
        ) {
          promiseList.push(this.uploadJsonFile(name, value[name]));
        } else {
          value[name] = "";
        }
      });

      Promise.all(promiseList)
        .then((res) => {
          res?.forEach(({ name, url }) => {
            value[name] = url;
          });

          this.drawerProps.type === "create"
            ? this._createEntity(tableId, value)
            : this._updateEntity(tableId, value);
        })
        .catch((error) => {
          this.$toast(error);
        });
    },
    _bindTemplateTable(tableDataId) {
      const methodName = this.isTk
        ? "SaveTKWorkTemplatTable"
        : "SaveWorkTemplatTable";
      // 安全校验方法是否存在
      const targetApi = apiModule[methodName] || apiModule.SaveWorkTemplatTable;
      targetApi({
        id: this.drawerProps.rowData.detailId,
        tableDataId: tableDataId,
      }).then((response) => {
        if (response.code === 200) {
          this.$toast("操作成功");
          setTimeout(() => {
            this.$emit("success");
          }, 1000);
        } else {
          this.$toast(response.msg);
        }
      });
    },
    _createEntity(tableId, value) {
      const processData = {};
      for (const [key, val] of Object.entries(value)) {
        const camelKey = this.processCamelCase(key);
        processData[camelKey] =
          typeof val === "object" || Array.isArray(val)
            ? JSON.stringify(val)
            : val;
      }

      let params = {
        tableId: tableId,
        dynamicData: processData,
      };

      CreateEntity(params).then((res) => {
        if (res.statusCode === 200) {
          this.$toast("创建成功!");
          this._bindTemplateTable(res.data);
        } else {
          this.$toast(res.message);
        }
      });
    },
    _updateEntity(tableId, value) {
      const processData = {};
      for (const [key, val] of Object.entries(value)) {
        const camelKey = this.processCamelCase(key);
        if (!val) continue;

        processData[camelKey] =
          typeof val === "object" || Array.isArray(val)
            ? JSON.stringify(val)
            : val;
      }

      if (this.formData.id) {
        processData["Id"] = this.formData.id;
      }

      let params = {
        tableId: tableId,
        dynamicData: processData,
      };

      let _this = this;
      UpdateEntity(params).then((res) => {
        if (res.statusCode === 200) {
          _this.$toast(`更新数据成功!`);
          _this._bindTemplateTable(_this.formData.id);
        } else {
          _this.$toast(res.message);
        }
      });
    },
  },
};
</script>

amis操作、表单渲染组件

<template>
  <div id="main-low-code" style="height: 100%;">
    <div class="drawer-title">{{ formData.name }}</div>
    <div id="content-low-code" style="padding: 12px; overflow: auto"></div>
  </div>
</template>

<script>
import Vue from "vue";
import "@/utils/customRenderer.js"; // 加载自定义渲染器

export default Vue.extend({
  props: {
    formData: {
      type: Object,
      default: () => {
        return {};
      },
    },
    formJson: {
      type: Object,
      default: () => {
        return {};
      },
    },
  },
  data() {
    return {
      amisScoped: null,
    };
  },
  watch: {
    formJson: {
      handler(data) {
        this.$nextTick(() => {
          this.onRendering(data);
        });
      },
      immediate: true,
      deep: true,
    },
  },
  mounted() {
    this.amis = window.amisRequire ? window.amisRequire("amis/embed") : null;
    console.log(this.amis);
    if (!this.amis) {
      console.error("amisRequire 未定义");
      return;
    }
    this.onRendering(this.formJson);
  },
  methods: {
    onRendering(data) {
      if (this.amis) {
        this.amisScoped = this.amis.embed("#content-low-code", data);
      }
    },
    submit() {
      if (this.amisScoped) {
        let text = this.amisScoped
          .getComponentByName(this.formData.code)
          .getValues();
        return text;
      }
      return null;
    },
  },
  beforeDestroy() {
    if (this.amisScoped) {
      this.amisScoped.unmount();
    }
  },
});
</script>

<style lang="scss" scoped>
.drawer-title {
  margin: 12px 0;
  font-size: 18px;
  font-weight: bold;
  color: #333333;
  text-align: center;
}
</style>

此版本更新内容包括:Feature :sparkles: 新增 sortBy 和 topAndOther filter (#1378) (#1379) api 新增 responseData 配置 (#1379) 添加季度选择器 Quarter (#1382) Container 支持设置样式 (#1411) 加入 ecStat, Apache ECharts (incubating) 的统计和数据挖掘工具 (#1419) Form 支持 feedback (#1420) 新增仿 antd 主题 (#1421) Enhancement jssdk 支持外部监控路由变化重新切换页面 (#1373) 选择类表单项 selectFirst 跳过 disabled 的选项 (#1393) iconfont 发布到 sdk 里 (#1395) api mock 地址替换 (#1408) Echarts 没数据时显示 loading (#1409) Breaking :翻译文件的 key 不再是中文,如果有修改过英文翻译,需要换成新 key (#1416) (#1418) 拆解 factory.tsx,添加 RootRenderer,并能 处理部分 action, 直接渲染个按钮也能弹窗,发ajax了 (#1425) Text 配置 source 样式优化 (#1429) 更换 autobind,继承时 this 不错乱 (#1433) Bugfix 修复 表单项在不配置 name 的时候,value 属性失效问题 (#1372) 修复 Excel 导出的列顺序依照配置的顺序,而不是数据源 (#1377) 修复 ChartRadios tooltip 问题. (#1378) 修复 位置选择组件在新版百度地图 api 下无法使用问题 (#1381) 修复 表单项有多个的时候,回车不提交问题 (#1387) 修复 helper 中 white-space 不正确问题 (#1390) 修复 Excel 导出不支持嵌套 name 和 tpl 问题 (#1424) 修复 收起状态导航菜单不可点击跳转问题 (#1428) 修复 Checkbox 无 disabled 样式问题 (#1414)amis前端低代码框架是一个低代码前端框架,它使用 JSON 配置来生成页面,可以节省页面开发工作量,极大提升开发前端页面的效率。 目前在百度广泛用于内部平台的前端开发,已有 100+ 部门使用,创建了 3w+ 页面。amis前端低代码框架特点1、不需要懂前端:在百度内部,大部分 amis 用户之前从来没写过前端页面,也不会 JavaScript,却能做出专业且复杂的后台界面,这是所有其他前端 UI 库都无法做到的; 2、不受前端技术更新的影响:百度内部最老的 amis 页面是 4 年多前创建的,至今还在使用,而当年的 Angular/Vue/React 版本现在都废弃了,当年流行的 Gulp 也被 Webpack 取代了,如果这些页面不是用 amis,现在的维护成本会很高; 3、享受 amis 的不断升级:amis 一直在提升细节交互体验,比如表格首行冻结、下拉框大数据下不卡顿等,之前的 JSON 配置完全不需要修改; 4、可以完全使用可视化页面编辑器 来制作页面:一般前端可视化编辑器只能用来做静态原型,而 amis 可视化编辑器做出的页面是可以直接上线的。 5、提供完整的界面解决方案:其它 UI 框架必须使用 JavaScript 来组装业务逻辑,而 amis 只需 JSON 配置就能完成完整功能开发,包括数据获取、表单提交及验证等功能,做出来的页面不需要经过二次开发就能直接上线; 6、内置 100+ 种 UI 组件:包括其它 UI 框架都不会提供的富文本编辑器、条件组合等,能满足各种页面组件展现的需求,而且对于特殊的展现形式还可以通过 自定义组件 来扩充; 7、容器支持无限级嵌套:可以通过组合来满足各种布局需求; 8、经历了长时间的实战考验:amis百度内部得到了广泛使用,在 4 年多的时间里创建了 3 万+ 页面,从内容审核到机器管理,从数据分析到模型训练,amis 满足了各种各样的页面需求,最复杂的页面有超过 1 万行 JSON 配置。
### 开源前端低代码开发平台 以下是几个知名的开源前端低代码开发平台,这些工具能够帮助开发者快速构建应用界面并满足特定业务需求: #### 1. **lowcode-engine** LowCodeEngine 是一款强大的低代码研发框架,专为低代码平台开发者设计。它提供了高度可扩展的能力,允许用户基于该引擎快速定制符合自身业务需求的低代码平台。此项目由阿里巴巴前端委员会与钉钉宜搭共同推出,适用于复杂的企业级应用场景[^1]。 #### 2. **Formily** Formily 是阿里巴巴推出的另一个开源项目,专注于表单生成领域。它支持动态表单渲染、复杂的校验规则以及自定义组件插槽等功能。Formily 提供了两种实现方式:React 和 Vue 版本,适合不同的技术栈需求[^2]。 #### 3. **Amis** Amis 是一款简单易用的低代码前端框架,核心理念是“配置即页面”。通过 JSON 配置文件描述 UI 结构,Amis 能够自动渲染出对应的交互式页面。它的优势在于学习成本较低,同时功能覆盖范围广泛,包括表格、图表、表单等多种场景[^2]。 #### 4. **DooringX** DoeringX 是一个模块化、可视化的 React 组件设计器,旨在降低前端开发门槛。它内置了大量的基础组件库,并支持拖拽操作完成页面布局的设计工作。对于希望打造个性化组件编辑器的团队来说是一个不错的选择[^2]。 #### 5. **Yaosparrow** Yaosparrow 是一个轻量级的 HTML5 应用程序搭建平台,采用所见即所得的方式让用户轻松创建跨终端的应用服务。其特点是以积木式的拼接方法代替传统编程模式,极大提高了生产效率。 --- ```javascript // 示例:如何使用 Amis 渲染一个简单的表单 const amisJsonSchema = { type: &#39;page&#39;, body: [ { type: &#39;form&#39;, title: &#39;登录表单&#39;, api: &#39;/api/login&#39;, // 表单提交接口地址 controls: [ { name: &#39;username&#39;, label: &#39;用户名&#39;, type: &#39;text&#39; }, { name: &#39;password&#39;, label: &#39;密码&#39;, type: &#39;password&#39; } ] } ] }; import AMIS from &#39;@amis/embed&#39;; AMIS.render(amisJsonSchema, document.getElementById(&#39;root&#39;)); ``` --- ### 总结 以上列举了几款优秀的开源前端低代码开发平台,每种都有各自的特点和技术侧重点。如果追求灵活性和企业级解决方案,则可以选择 `lowcode-engine` 或者 `Formily`;而如果是更倾向于简易性和快速上手的话,那么像 `Amis` 这样的工具会更加合适一些。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值