

<template>
<div class="bg-#fff" px20px py30px h-full>
<div w-full f-c justify-between mb20px pb10px style="border-bottom: 1px solid #cccccc">
<div flex>
<div class="cur">Form builder</div>
</div>
<div f-c>
<a-button type="outline">保存</a-button>
</div>
</div>
<div class="low-code-editor" flex>
<Toolbox />
<Canvas v-model:components="components" @select="selectedId = $event" />
<div class="json-output">
<pre>{{ JSON.stringify(components, null, 2) }}</pre>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import Toolbox from './components/Toolbox.vue'
import Canvas from './components/Canvas.vue'
const components = ref<any[]>([]) // 存储画布中的组件
const selectedId = ref<string | null>(null)
</script>
<style scoped lang="less">
.low-code-editor {
height: 92%;
background-color: #f3f3f3;
padding: 20px;
}
.json-output {
width: 300px;
padding: 10px;
background: #f5f5f5;
overflow-y: auto;
}
.cur {
color: rgb(15, 15, 15);
font-weight: 600;
position: relative;
}
.cur::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -20px;
width: 100%;
height: 2px;
background-color: #3874cb;
}
:deep(.arco-list-item) {
&:hover {
background: #dddddd; /* 更深的蓝色 */
}
}
</style>

Toolbox
<template>
<div h-full mr20px>
<a-card class="card-demo" title="Form details" w-220px mb20px hoverable>
<BaseFormItem label="Form name (json)" tooltip="表单名称不能留空">
<a-input v-model="formData.name" />
</BaseFormItem>
</a-card>
<a-card class="card-demo" title="Components" w-220px hoverable>
<a-list size="small">
<a-list-item v-for="item in toolboxItems" :key="item.type" draggable="true" @dragstart="onDragStart($event, item.type)">
<div f-c>
<svg width="20" height="20" viewBox="0 0 24 24" class="icon">
<g v-html="item.icon"></g>
</svg>
<div ml10px>{{ item.label }}</div>
</div>
</a-list-item>
</a-list>
</a-card>
</div>
</template>
<script setup lang="ts">
import BaseFormItem from '@/components/BaseFormItem/index.vue'
import { generateThreeDigitNumber } from '@/utils/tool'
import { onMounted, ref } from 'vue'
const formData = ref({
name: `new_template_${generateThreeDigitNumber()}`,
})
const toolboxItems = [
{
type: 'a-switch',
label: 'Boolean',
icon: '<path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm-1 14H6v-2h12v2z" fill="currentColor"/>',
},
{
type: 'a-switch',
label: 'Multiple Choice',
icon: '<path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zM8 10l2 2 5-5m-7 7h8" fill="none" stroke="currentColor" stroke-width="2"/>',
},
{
type: 'a-switch',
label: 'Date',
icon: '<path d="M19 4h-1V2h-2v2H8V2H6v2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 16H5V9h14v11z" fill="currentColor"/>',
},
{
type: 'a-switch',
label: 'Date + Time',
icon: '<path d="M19 4h-1V2h-2v2H8V2H6v2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 16H5V9h14v11zm-7-7V9H9v4H7v2h5v-2z" fill="currentColor"/>',
},
{
type: 'a-switch',
label: 'Description Text',
icon: '<path d="M3 4h18v2H3V4zm0 4h18v2H3V8zm0 4h12v2H3v-2zm0 4h18v2H3v-2z" fill="currentColor"/>',
},
{
type: 'a-switch',
label: 'Image',
icon: '<path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 16H5V5h14v14zm-9-8l-3 3-2-2-3 3V7h8v4z" fill="currentColor"/>',
},
{
type: 'a-switch',
label: 'Number Field',
icon: '<path d="M3 4h18v2H3V4zm6 4h6v2H9V8zm-6 4h18v2H3v-2zm6 4h6v2H9v-2z" fill="currentColor"/>',
},
{ type: 'a-switch', label: 'Text', icon: '<path d="M3 4h18v2H3V4zm0 4h12v2H3V8zm0 4h18v2H3v-2z" fill="currentColor"/>' },
{
type: 'a-switch',
label: 'Time',
icon: '<path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8 8 0 1 1 0-16 8 8 0 0 1 0 16zm-1-12v5l4 2 1-1-3-2V8h-2z" fill="currentColor"/>',
},
{
type: 'a-switch',
label: 'Video',
icon: '<path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm-1 14H6V7h12v10zm-7-7l5 3-5 3V10z" fill="currentColor"/>',
},
{
type: 'a-switch',
label: 'Radio',
icon: '<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="4" fill="currentColor"/>',
},
]
const onDragStart = (event: DragEvent, type: string) => {
event.dataTransfer?.setData('componentType', type)
}
</script>
<style scoped>
.card-demo {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
:deep(.arco-list-content) {
height: 390px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
}
scrollbar-width: none;
-ms-overflow-style: none;
}
}
</style>

import { generateThreeDigitNumber } from '@/utils/tool'
export interface ComponentConfig {
uiType: string
wordType: string
props: Record<string, any>
params:any
}
export const componentConfigs: Record<string, any> = {
'a-switch': {
uiType: 'a-switch',
wordType: 'boolean',
props: {
disabled: true,
modelValue: false
},
params: {
fieldName: '',
lable: '',
default: false,
readonly: false,
required: false,
},
},
}
export const getComponentConfig = (type: string): ComponentConfig => {
return componentConfigs[type]
}
<template>
<a-card title="画布" style="flex: 1; min-height: 400px">
<div class="canvas-area" @drop="onDrop" @dragover.prevent="onDragOver" @dragenter.prevent>
<div v-if="props.components.length === 0" class="placeholder">拖拽组件到这里</div>
<div v-else>
<VueDraggable v-model="draggableComponents" item-key="id">
<component-renderer
v-for="comp in props.components"
:key="comp.id"
:component="comp"
:componentsArr="components"
@remove="removeItem"
@changeComponent="changeComponent"
/>
</VueDraggable>
</div>
</div>
</a-card>
</template>
<script setup lang="ts">
import { defineProps, defineEmits, computed } from 'vue'
import ComponentRenderer from './ComponentRenderer.vue'
import { getComponentConfig } from '../tool'
import { generateRandomNumber, generateThreeDigitNumber } from '@/utils/tool'
import { VueDraggable } from 'vue-draggable-plus'
import { ComponentType } from '@/types/humanTemplates'
const props = defineProps<{
components: ComponentType[]
}>()
const emit = defineEmits(['update:components'])
const draggableComponents = computed({
get() {
return props.components
},
set(newValue) {
emit('update:components', newValue)
},
})
const onDragOver = (event: DragEvent) => {
event.preventDefault()
}
const onDrop = (event: DragEvent) => {
event.preventDefault()
const type = event.dataTransfer?.getData('componentType')
if (type) {
const config: any = getComponentConfig(type)
const newComponent = {
id: `comp${Date.now()}${generateThreeDigitNumber()}`,
...config,
params: { ...config.params },
}
newComponent.params.fieldName = `field_${config.wordType}_${generateRandomNumber()}`
emit('update:components', [...props.components, newComponent])
}
}
const removeItem = (id: string) => {
const updatedComponents = props.components.filter((comp) => comp.id !== id)
emit('update:components', updatedComponents)
}
const changeComponent = (params: ComponentType) => {
const updatedComponents = props.components.map((comp) => {
if (comp.id === params.id) {
return {
...comp,
...params,
}
}
return comp
})
emit('update:components', updatedComponents)
}
</script>
<style scoped>
.canvas-area {
height: 540px;
border: 1px solid #ccc;
}
.placeholder {
text-align: center;
color: #999;
line-height: 400px;
}
</style>
ComponentRenderer
<template>
<div class="component-wrapper" f-c justify-between>
<div f-c>
<icon-drag-dot-vertical mr10px class="h-5 w-5 text-gray-500 cursor-grab" />
<BaseFormItem v-if="component.params.lable" :label="component.params.lable" w-300px>
<component :is="componentType" v-bind="component.props"></component>
</BaseFormItem>
<component v-else :is="componentType" v-bind="component.props"></component>
</div>
<div f-c>
<icon-edit text-20px mr10px @click="componentModalShow = true" />
<icon-delete text-20px @click.stop="$emit('remove', component.id)" />
</div>
</div>
<!-- component modal -->
<ComponentModal v-model="componentModalShow" :component="component" @changeComponent="changeComponent" />
</template>
<script setup lang="ts">
import { computed, defineProps, ref, watch } from 'vue'
import { Button, Input, Switch, Checkbox, DatePicker, TimePicker, Radio } from '@arco-design/web-vue'
import ComponentModal from './ComponentModal.vue'
import BaseFormItem from '@/components/BaseFormItem/index.vue'
import { ComponentType } from '@/types/humanTemplates'
const props = defineProps<{
component: ComponentType
componentsArr: ComponentType[]
}>()
const emit = defineEmits(['remove', 'changeComponent'])
const componentModalShow = ref(false)
const componentMap: Record<string, any> = {
'a-switch': Switch,
}
const componentType = computed(() => {
return componentMap[props.component.uiType] || 'div'
})
const changeComponent = (params: ComponentType) => {
emit('changeComponent', { ...params })
}
</script>
<style scoped lang="less">
.component-wrapper {
margin: 8px;
padding: 4px;
border: 1px dashed #ccc;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.component-wrapper:hover {
border-color: #4a9dd5;
}
.delete-icon {
width: 16px;
height: 16px;
}
</style>
ComponentModal
<template>
<a-modal
v-if="modelValue"
:width="600"
title="EDIT COMPONENT"
:visible="modelValue"
:body-style="{ paddingLeft: 0, paddingRight: 0 }"
ok-text="保存"
@ok="handleOk"
@cancel="cancel()"
>
<BaseForm ref="formRef" :model="componentParams.params" layout="vertical" px20px>
<div f-c>
<BaseFormItem
label="fieldName"
tooltip="This will populate in the human task form in your workflow definition and appear as an Input parameter."
mr20px
>
<a-input v-model="componentParams.params.fieldName" />
</BaseFormItem>
<BaseFormItem
label="lable"
tooltip="This will populate in the human task form in your workflow definition and appear as an Input parameter."
>
<a-input v-model="componentParams.params.lable" />
</BaseFormItem>
</div>
<div f-c w300px mt10px>
<a-switch v-model="componentParams.params.default">
<template #checked>ON</template>
<template #unchecked>OFF</template>
</a-switch>
<div ml10px>Boolean default value</div>
</div>
<div f-c mt10px>
<div f-c>
<a-switch v-model="componentParams.params.readonly">
<template #checked>ON</template>
<template #unchecked>OFF</template>
</a-switch>
<div ml10px>Required</div>
</div>
<div f-c ml20px>
<a-switch v-model="componentParams.params.required">
<template #checked>ON</template>
<template #unchecked>OFF</template>
</a-switch>
<div ml10px>Read-only</div>
</div>
</div>
</BaseForm>
<div class="bg-#e5e6eb" w-full h1px my20px></div>
<div px20px>
<div mb10px font-600>预览</div>
<div v-if="componentParams.uiType === 'a-switch'">
<BaseFormItem v-if="componentParams.params.lable" :label="componentParams.params.lable" w-300px>
<a-switch v-model="componentParams.params.default" disabled>
<template #checked>ON</template>
<template #unchecked>OFF</template>
</a-switch>
</BaseFormItem>
<a-switch v-else v-model="componentParams.params.default" disabled>
<template #checked>ON</template>
<template #unchecked>OFF</template>
</a-switch>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import BaseForm from '@/components/BaseForm/index.vue'
import BaseFormItem from '@/components/BaseFormItem/index.vue'
import { ComponentType } from '@/types/humanTemplates'
import { ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
component: {
type: Object,
default: () => {},
},
})
const emit = defineEmits({
'update:modelValue': (value: boolean) => value,
changeComponent: (data: ComponentType) => data,
})
const formRef = ref()
const componentParams = ref({} as ComponentType)
const cancel = () => {
emit('update:modelValue', false)
}
const handleOk = async () => {
cancel()
const modelValue = componentParams.value.params.default
const dataParams = {
...componentParams.value,
props: {
...componentParams.value.props,
modelValue,
},
}
emit('changeComponent', { ...dataParams })
console.log('🚀 ~ handleOk ~ componentParams.value:', dataParams)
}
watch(
() => props.modelValue,
(newData: boolean) => {
if (newData) {
componentParams.value = JSON.parse(JSON.stringify(props.component))
} else {
componentParams.value = JSON.parse(JSON.stringify({}))
}
},
{ immediate: true }
)
</script>
<style></style>