文章目录
完成实现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>