低代码平台详细技术方案 - 前端架构
基于 Vue 3 + TypeScript + Element Plus + Pinia
目录
一、项目整体结构
1.1 目录结构
bash
low-code-frontend/
├── src/
│ ├── main.ts # 应用入口
│ ├── App.vue # 根组件
│ │
│ ├── views/ # 页面视图
│ │ ├── designer/ # 设计器页面
│ │ │ ├── PageDesigner.vue # 页面设计器
│ │ │ ├── FormDesigner.vue # 表单设计器
│ │ │ ├── FlowDesigner.vue # 流程设计器
│ │ │ └── ModelDesigner.vue # 数据模型设计器
│ │ ├── runtime/ # 运行时页面
│ │ │ ├── DynamicPage.vue # 动态页面渲染
│ │ │ ├── FormPage.vue # 表单页面
│ │ │ └── WorkflowPage.vue # 流程页面
│ │ └── admin/ # 管理后台
│ │ ├── UserManagement.vue
│ │ ├── RoleManagement.vue
│ │ └── PageManagement.vue
│ │
│ ├── components/ # 组件
│ │ ├── designer/ # 设计器组件
│ │ │ ├── Canvas/ # 画布
│ │ │ │ ├── Canvas.vue
│ │ │ │ ├── DropZone.vue
│ │ │ │ └── SelectBox.vue
│ │ │ ├── Toolbar/ # 工具栏
│ │ │ │ └── Toolbar.vue
│ │ │ ├── ComponentPanel/ # 组件面板
│ │ │ │ ├── ComponentPanel.vue
│ │ │ │ ├── ComponentGroup.vue
│ │ │ │ └── ComponentItem.vue
│ │ │ ├── PropertyPanel/ # 属性面板
│ │ │ │ ├── PropertyPanel.vue
│ │ │ │ ├── PropertyForm.vue
│ │ │ │ └── EventEditor.vue
│ │ │ ├── LayerPanel/ # 图层面板
│ │ │ │ └── LayerPanel.vue
│ │ │ └── FlowDesigner/ # 流程设计器
│ │ │ ├── FlowCanvas.vue
│ │ │ ├── FlowNode.vue
│ │ │ └── FlowEdge.vue
│ │ │
│ │ ├── runtime/ # 运行时组件
│ │ │ ├── DynamicRenderer/ # 动态渲染器
│ │ │ │ ├── Renderer.vue
│ │ │ │ └── ComponentLoader.vue
│ │ │ ├── DynamicForm/ # 动态表单
│ │ │ │ ├── FormRenderer.vue
│ │ │ │ ├── FormItem.vue
│ │ │ │ └── FieldComponent.vue
│ │ │ └── WorkflowViewer/ # 流程查看器
│ │ │ ├── FlowViewer.vue
│ │ │ └── TaskList.vue
│ │ │
│ │ ├── widgets/ # 低代码组件库
│ │ │ ├── layout/ # 布局组件
│ │ │ │ ├── Container.vue
│ │ │ │ ├── Row.vue
│ │ │ │ ├── Col.vue
│ │ │ │ └── Card.vue
│ │ │ ├── form/ # 表单组件
│ │ │ │ ├── Input.vue
│ │ │ │ ├── Select.vue
│ │ │ │ ├── DatePicker.vue
│ │ │ │ └── Upload.vue
│ │ │ ├── data/ # 数据展示组件
│ │ │ │ ├── Table.vue
│ │ │ │ ├── List.vue
│ │ │ │ ├── Tree.vue
│ │ │ │ └── Pagination.vue
│ │ │ ├── chart/ # 图表组件
│ │ │ │ ├── LineChart.vue
│ │ │ │ ├── BarChart.vue
│ │ │ │ └── PieChart.vue
│ │ │ └── custom/ # 自定义组件
│ │ │ └── CustomWidget.vue
│ │ │
│ │ └── common/ # 公共组件
│ │ ├── Loading.vue
│ │ ├── Empty.vue
│ │ └── ErrorBoundary.vue
│ │
│ ├── stores/ # Pinia 状态管理
│ │ ├── designer.ts # 设计器状态
│ │ ├── metadata.ts # 元数据状态
│ │ ├── runtime.ts # 运行时状态
│ │ ├── user.ts # 用户状态
│ │ └── app.ts # 应用全局状态
│ │
│ ├── api/ # API 接口
│ │ ├── request.ts # 请求封装
│ │ ├── metadata.ts # 元数据 API
│ │ ├── form.ts # 表单 API
│ │ ├── data.ts # 数据 API
│ │ ├── workflow.ts # 流程 API
│ │ └── auth.ts # 认证 API
│ │
│ ├── engine/ # 引擎
│ │ ├── renderer/ # 渲染引擎
│ │ │ ├── index.ts
│ │ │ ├── component-factory.ts
│ │ │ └── props-parser.ts
│ │ ├── evaluator/ # 表达式求值器
│ │ │ ├── index.ts
│ │ │ └── functions.ts
│ │ ├── validator/ # 校验器
│ │ │ └── index.ts
│ │ └── event/ # 事件引擎
│ │ ├── index.ts
│ │ └── event-bus.ts
│ │
│ ├── schemas/ # Schema 定义
│ │ ├── component-schema.ts # 组件 Schema
│ │ ├── page-schema.ts # 页面 Schema
│ │ ├── form-schema.ts # 表单 Schema
│ │ └── validation-schema.ts # 校验 Schema
│ │
│ ├── utils/ # 工具函数
│ │ ├── uuid.ts # UUID 生成
│ │ ├── clone.ts # 深拷贝
│ │ ├── storage.ts # 本地存储
│ │ ├── download.ts # 文件下载
│ │ └── date.ts # 日期处理
│ │
│ ├── composables/ # 组合式函数
│ │ ├── useDesigner.ts # 设计器逻辑
│ │ ├── useRenderer.ts # 渲染器逻辑
│ │ ├── useValidator.ts # 校验逻辑
│ │ ├── useDrag.ts # 拖拽逻辑
│ │ └── usePermission.ts # 权限逻辑
│ │
│ ├── directives/ # 自定义指令
│ │ ├── permission.ts # 权限指令
│ │ ├── loading.ts # 加载指令
│ │ └── draggable.ts # 拖拽指令
│ │
│ ├── router/ # 路由
│ │ ├── index.ts
│ │ ├── routes.ts
│ │ └── guards.ts # 路由守卫
│ │
│ ├── styles/ # 样式
│ │ ├── index.scss # 全局样式
│ │ ├── variables.scss # 变量
│ │ ├── mixins.scss # 混入
│ │ └── themes/ # 主题
│ │ ├── default.scss
│ │ └── dark.scss
│ │
│ ├── types/ # TypeScript 类型定义
│ │ ├── component.ts
│ │ ├── page.ts
│ │ ├── form.ts
│ │ ├── workflow.ts
│ │ └── api.ts
│ │
│ └── config/ # 配置文件
│ ├── app.config.ts
│ └── widget.config.ts
│
├── public/ # 静态资源
│ ├── favicon.ico
│ └── config.json # 运行时配置
│
├── tests/ # 测试
│ ├── unit/ # 单元测试
│ └── e2e/ # E2E 测试
│
├── index.html # HTML 入口
├── vite.config.ts # Vite 配置
├── tsconfig.json # TypeScript 配置
├── package.json
└── README.md1.2 技术栈选型
json
{
"dependencies": {
"vue": "^3.3.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"element-plus": "^2.4.0",
"axios": "^1.5.0",
"dayjs": "^1.11.0",
"lodash-es": "^4.17.21",
"vuedraggable": "^4.1.0",
"sortablejs": "^1.15.0",
"@logicflow/core": "^1.2.0",
"@logicflow/extension": "^1.2.0",
"echarts": "^5.4.0",
"vue-echarts": "^6.6.0",
"monaco-editor": "^0.44.0",
"monaco-editor-webpack-plugin": "^7.1.0",
"crypto-js": "^4.2.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.4.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"vite": "^4.5.0",
"typescript": "^5.2.0",
"@types/node": "^20.8.0",
"@types/lodash-es": "^4.17.10",
"sass": "^1.69.0",
"vitest": "^0.34.0",
"@vue/test-utils": "^2.4.0",
"playwright": "^1.39.0",
"eslint": "^8.52.0",
"prettier": "^3.0.0"
}
}
```bash
---
## 二、设计器架构
### 2.1 整体布局┌─────────────────────────────────────────────────────┐ │ Toolbar(工具栏) │ │ [保存] [预览] [发布] [撤销] [重做] [清空] │ └─────────────────────────────────────────────────────┘ ┌───────────┬────────────────────────┬────────────────┐ │ │ │ │ │ Component │ │ Property │ │ Panel │ Canvas │ Panel │ │ │ (画布区) │ (属性面板) │ │ 组件列表 │ │ │ │ │ [拖拽放置组件] │ [编辑属性] │ │ │ │ [事件配置] │ │ │ │ [样式设置] │ │ │ │ │ ├───────────┤ ├────────────────┤ │ │ │ │ │ Layer │ │ Layer │ │ Panel │ │ Tree │ │ │ │ │ │ 图层列表 │ │ [组件层级] │ │ │ │ │ └───────────┴────────────────────────┴────────────────┘
bash
### 2.2 核心组件设计
#### 1. 画布组件 (Canvas.vue)
**功能:**
- 组件拖拽放置
- 组件选中、移动、缩放
- 多选和对齐
- 网格吸附
- 缩放和平移
**关键实现:**
```typescript
// Canvas.vue
interface CanvasState {
components: ComponentInstance[] // 画布上的组件列表
selectedIds: string[] // 选中的组件ID
scale: number // 缩放比例
offset: { x: number; y: number } // 画布偏移
}
// 组件实例定义
interface ComponentInstance {
id: string
type: string // 组件类型:el-input, el-table...
props: Record<string, any>
style: {
position: 'absolute'
left: number
top: number
width: number
height: number
}
children?: ComponentInstance[]
}
// 拖拽处理
const handleDrop = (e: DragEvent) => {
const componentType = e.dataTransfer?.getData('componentType')
const { x, y } = getDropPosition(e)
const newComponent: ComponentInstance = {
id: generateId(),
type: componentType,
props: getDefaultProps(componentType),
style: {
position: 'absolute',
left: x,
top: y,
width: 200,
height: 40
}
}
canvasState.components.push(newComponent)
}
// 选中组件
const selectComponent = (id: string, multiple = false) => {
if (multiple) {
if (canvasState.selectedIds.includes(id)) {
canvasState.selectedIds = canvasState.selectedIds.filter(i => i !== id)
} else {
canvasState.selectedIds.push(id)
}
} else {
canvasState.selectedIds = [id]
}
}2. 组件面板 (ComponentPanel.vue)
功能:
- 组件分类展示
- 组件搜索
- 拖拽到画布
组件分类:
typescript
interface ComponentCategory {
name: string
label: string
icon: string
components: ComponentMeta[]
}
const componentCategories: ComponentCategory[] = [
{
name: 'layout',
label: '布局',
icon: 'Layout',
components: [
{ type: 'container', label: '容器', icon: 'Box' },
{ type: 'row', label: '行', icon: 'Grid' },
{ type: 'col', label: '列', icon: 'Columns' }
]
},
{
name: 'form',
label: '表单',
icon: 'Edit',
components: [
{ type: 'el-input', label: '输入框', icon: 'Edit' },
{ type: 'el-select', label: '下拉框', icon: 'ArrowDown' },
{ type: 'el-date-picker', label: '日期选择', icon: 'Calendar' },
{ type: 'el-upload', label: '文件上传', icon: 'Upload' }
]
},
{
name: 'data',
label: '数据展示',
icon: 'Grid',
components: [
{ type: 'el-table', label: '表格', icon: 'Table' },
{ type: 'el-tree', label: '树形', icon: 'Tree' },
{ type: 'el-pagination', label: '分页', icon: 'Grid' }
]
},
{
name: 'chart',
label: '图表',
icon: 'TrendingUp',
components: [
{ type: 'line-chart', label: '折线图', icon: 'TrendingUp' },
{ type: 'bar-chart', label: '柱状图', icon: 'BarChart' },
{ type: 'pie-chart', label: '饼图', icon: 'PieChart' }
]
}
]
```bash
#### 3. 属性面板 (PropertyPanel.vue)
**功能:**
- 动态属性表单
- 样式编辑器
- 事件配置
**关键实现:**
```typescript
// 属性配置 Schema
interface PropertySchema {
label: string
name: string
type: 'input' | 'select' | 'number' | 'switch' | 'color' | 'code'
options?: { label: string; value: any }[]
defaultValue?: any
group?: string // 属性分组
}
// 示例:el-input 的属性配置
const inputPropertySchema: PropertySchema[] = [
// 基础属性
{ label: '字段名', name: 'field', type: 'input', group: 'basic' },
{ label: '标签', name: 'label', type: 'input', group: 'basic' },
{ label: '占位符', name: 'placeholder', type: 'input', group: 'basic' },
{ label: '默认值', name: 'defaultValue', type: 'input', group: 'basic' },
// 样式属性
{ label: '宽度', name: 'width', type: 'input', group: 'style' },
{ label: '尺寸', name: 'size', type: 'select', group: 'style', options: [
{ label: '大', value: 'large' },
{ label: '默认', value: 'default' },
{ label: '小', value: 'small' }
]},
// 状态属性
{ label: '禁用', name: 'disabled', type: 'switch', group: 'state' },
{ label: '只读', name: 'readonly', type: 'switch', group: 'state' },
{ label: '清除按钮', name: 'clearable', type: 'switch', group: 'state' },
// 校验规则
{ label: '必填', name: 'required', type: 'switch', group: 'validation' },
{ label: '最小长度', name: 'minLength', type: 'number', group: 'validation' },
{ label: '最大长度', name: 'maxLength', type: 'number', group: 'validation' },
{ label: '正则表达式', name: 'pattern', type: 'input', group: 'validation' }
]
// 事件配置
interface EventConfig {
name: string // 事件名:change, click...
handler: string // 处理函数
actionType: 'api' | 'navigate' | 'openDialog' | 'custom'
actionConfig: any
}
const eventSchema = [
{ name: 'change', label: '值改变' },
{ name: 'blur', label: '失去焦点' },
{ name: 'focus', label: '获得焦点' },
{ name: 'clear', label: '清空' }
]4. 流程设计器 (FlowDesigner.vue)
基于 LogicFlow 实现:
typescript
import LogicFlow from '@logicflow/core'
import { Menu, DndPanel, SelectionSelect } from '@logicflow/extension'
// 初始化流程设计器
const initFlowDesigner = () => {
const lf = new LogicFlow({
container: document.querySelector('#flow-container'),
grid: true,
plugins: [Menu, DndPanel, SelectionSelect],
// 自定义节点类型
nodeTypeMap: {
'start': StartNode,
'task': TaskNode,
'gateway': GatewayNode,
'end': EndNode
}
})
// 注册节点
lf.register('start', StartNode)
lf.register('task', TaskNode)
return lf
}
// 自定义任务节点
class TaskNode extends RectNode {
static extendKey = 'TaskNode'
getShape() {
const { width, height, x, y } = this.props
return {
type: 'rect',
x,
y,
width,
height,
rx: 4,
ry: 4
}
}
}
// 保存流程定义
const saveWorkflow = async () => {
const data = lf.getGraphData() // 获取流程数据
const bpmnXML = lf.getBpmnXML() // 转换为 BPMN XML
await api.saveWorkflow({
name: workflowName.value,
config: data,
bpmnXML
})
}
```bash
---
## 三、运行时引擎
### 3.1 渲染引擎设计
#### 核心渲染器 (Renderer.vue)
```vue
<template>
<component
:is="getComponent(schema.type)"
v-bind="parseProps(schema.props)"
v-on="parseEvents(schema.events)"
:style="parseStyle(schema.style)"
>
<template v-if="schema.children">
<Renderer
v-for="child in schema.children"
:key="child.id"
:schema="child"
:context="context"
/>
</template>
<template v-if="schema.slot">
{{ evaluateExpression(schema.slot, context) }}
</template>
</component>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { getComponent } from '@/engine/renderer/component-factory'
import { parseProps, parseEvents, parseStyle } from '@/engine/renderer/props-parser'
import { evaluateExpression } from '@/engine/evaluator'
interface Props {
schema: ComponentSchema
context: RenderContext
}
const props = defineProps<Props>()
// ComponentSchema 结构
interface ComponentSchema {
id: string
type: string
props?: Record<string, any>
events?: Record<string, EventConfig>
style?: Record<string, any>
children?: ComponentSchema[]
slot?: string
visible?: string // 可见性表达式
disabled?: string // 禁用表达式
}
interface RenderContext {
data: Record<string, any> // 表单数据
vars: Record<string, any> // 页面变量
methods: Record<string, Function>
}
</script>组件工厂 (component-factory.ts)
typescript
import { Component } from 'vue'
import * as ElementPlus from 'element-plus'
import * as CustomWidgets from '@/components/widgets'
// 组件注册表
const componentRegistry = new Map<string, Component>()
// 注册 Element Plus 组件
Object.entries(ElementPlus).forEach(([name, component]) => {
if (name.startsWith('El')) {
const kebabName = name.replace(/^El/, 'el-').replace(/([A-Z])/g, '-$1').toLowerCase()
componentRegistry.set(kebabName, component as Component)
}
})
// 注册自定义组件
Object.entries(CustomWidgets).forEach(([name, component]) => {
componentRegistry.set(name, component as Component)
})
// 获取组件
export const getComponent = (type: string): Component => {
const component = componentRegistry.get(type)
if (!component) {
console.warn(`Component "${type}" not found, using fallback`)
return 'div' as any
}
return component
}
// 动态注册组件
export const registerComponent = (type: string, component: Component) => {
componentRegistry.set(type, component)
}
```bash
#### 属性解析器 (props-parser.ts)
```typescript
import { evaluateExpression } from '@/engine/evaluator'
// 解析属性
export const parseProps = (
props: Record<string, any>,
context: RenderContext
): Record<string, any> => {
const parsed: Record<string, any> = {}
for (const [key, value] of Object.entries(props || {})) {
// 如果是表达式(以 {{ 开头)
if (typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}')) {
const expression = value.slice(2, -2).trim()
parsed[key] = evaluateExpression(expression, context)
}
// 如果是绑定(以 : 开头)
else if (typeof value === 'string' && value.startsWith(':')) {
const path = value.slice(1)
parsed[key] = getValueByPath(context.data, path)
}
// 普通值
else {
parsed[key] = value
}
}
return parsed
}
// 解析事件
export const parseEvents = (
events: Record<string, EventConfig>,
context: RenderContext
): Record<string, Function> => {
const parsed: Record<string, Function> = {}
for (const [eventName, config] of Object.entries(events || {})) {
parsed[eventName] = (...args: any[]) => {
handleEvent(config, context, args)
}
}
return parsed
}
// 事件处理
const handleEvent = async (
config: EventConfig,
context: RenderContext,
args: any[]
) => {
switch (config.actionType) {
case 'api':
// 调用接口
await callApi(config.actionConfig, context)
break
case 'navigate':
// 路由跳转
router.push(config.actionConfig.path)
break
case 'openDialog':
// 打开弹窗
openDialog(config.actionConfig)
break
case 'custom':
// 执行自定义函数
executeCustomFunction(config.actionConfig.code, context, args)
break
}
}3.2 表达式求值器 (evaluator/index.ts)
typescript
import { create, all } from 'mathjs'
const math = create(all)
// 表达式求值器
export class Evaluator {
private context: Record<string, any>
constructor(context: Record<string, any>) {
this.context = context
}
// 求值
eval(expression: string): any {
try {
// 替换上下文变量
const processed = this.preprocessExpression(expression)
// 使用 mathjs 或 Function 求值
return math.evaluate(processed, this.context)
} catch (error) {
console.error('Expression evaluation failed:', expression, error)
return undefined
}
}
// 预处理表达式
private preprocessExpression(expr: string): string {
// 替换 form.name -> context.data.name
return expr
.replace(/\bform\.(\w+)/g, 'data.$1')
.replace(/\bvars\.(\w+)/g, 'vars.$1')
}
}
// 辅助函数
export const evaluateExpression = (
expression: string,
context: RenderContext
): any => {
const evaluator = new Evaluator({
data: context.data,
vars: context.vars,
...builtinFunctions
})
return evaluator.eval(expression)
}
// 内置函数
const builtinFunctions = {
// 日期函数
now: () => new Date(),
formatDate: (date: Date, format: string) => dayjs(date).format(format),
// 字符串函数
concat: (...args: any[]) => args.join(''),
upper: (str: string) => str.toUpperCase(),
lower: (str: string) => str.toLowerCase(),
// 数组函数
sum: (arr: number[]) => arr.reduce((a, b) => a + b, 0),
avg: (arr: number[]) => arr.reduce((a, b) => a + b, 0) / arr.length,
// 逻辑函数
if: (condition: boolean, trueValue: any, falseValue: any) =>
condition ? trueValue : falseValue
}
```bash
### 3.3 表单校验引擎
```typescript
import type { RuleItem } from 'async-validator'
import Schema from 'async-validator'
// 表单校验
export const validateForm = async (
formSchema: FormSchema,
formData: Record<string, any>
): Promise<{ valid: boolean; errors: Record<string, string[]> }> => {
const rules: Record<string, RuleItem[]> = {}
// 构建校验规则
for (const field of formSchema.fields) {
// 检查字段是否可见
if (field.visible) {
const visible = evaluateExpression(field.visible, { data: formData })
if (!visible) continue
}
const fieldRules: RuleItem[] = []
for (const rule of field.rules || []) {
switch (rule.type) {
case 'required':
fieldRules.push({
required: true,
message: rule.message || `${field.label}不能为空`
})
break
case 'pattern':
fieldRules.push({
pattern: new RegExp(rule.pattern),
message: rule.message || `${field.label}格式不正确`
})
break
case 'min':
fieldRules.push({
min: rule.value,
message: rule.message || `${field.label}最小长度为${rule.value}`
})
break
case 'max':
fieldRules.push({
max: rule.value,
message: rule.message || `${field.label}最大长度为${rule.value}`
})
break
case 'custom':
fieldRules.push({
validator: (rule, value, callback) => {
const valid = executeValidator(rule.validator, value, formData)
if (!valid) {
callback(new Error(rule.message))
} else {
callback()
}
}
})
break
}
}
if (fieldRules.length > 0) {
rules[field.key] = fieldRules
}
}
// 执行校验
const validator = new Schema(rules)
try {
await validator.validate(formData)
return { valid: true, errors: {} }
} catch (error: any) {
const errors: Record<string, string[]> = {}
error.errors.forEach((err: any) => {
if (!errors[err.field]) {
errors[err.field] = []
}
errors[err.field].push(err.message)
})
return { valid: false, errors }
}
}四、状态管理(Pinia)
4.1 设计器状态 (designer.ts)
typescript
import { defineStore } from 'pinia'
import { cloneDeep } from 'lodash-es'
export const useDesignerStore = defineStore('designer', {
state: () => ({
// 画布状态
canvas: {
components: [] as ComponentInstance[],
selectedIds: [] as string[],
scale: 1,
offset: { x: 0, y: 0 }
},
// 历史记录(撤销/重做)
history: {
past: [] as ComponentInstance[][],
future: [] as ComponentInstance[][]
},
// 拖拽状态
dragging: {
isDragging: false,
componentType: null as string | null
},
// 配置状态
config: {
showGrid: true,
snapToGrid: true,
gridSize: 10
}
}),
getters: {
// 选中的组件
selectedComponents: (state) => {
return state.canvas.components.filter(c =>
state.canvas.selectedIds.includes(c.id)
)
},
// 第一个选中的组件
activeComponent: (state) => {
const id = state.canvas.selectedIds[0]
return state.canvas.components.find(c => c.id === id)
},
// 是否可以撤销
canUndo: (state) => state.history.past.length > 0,
// 是否可以重做
canRedo: (state) => state.history.future.length > 0
},
actions: {
// 添加组件
addComponent(component: ComponentInstance) {
this.recordHistory()
this.canvas.components.push(component)
this.canvas.selectedIds = [component.id]
},
// 删除组件
deleteComponent(id: string) {
this.recordHistory()
const index = this.canvas.components.findIndex(c => c.id === id)
if (index !== -1) {
this.canvas.components.splice(index, 1)
this.canvas.selectedIds = this.canvas.selectedIds.filter(i => i !== id)
}
},
// 更新组件属性
updateComponent(id: string, updates: Partial<ComponentInstance>) {
this.recordHistory()
const component = this.canvas.components.find(c => c.id === id)
if (component) {
Object.assign(component, updates)
}
},
// 选中组件
selectComponent(id: string, multiple = false) {
if (multiple) {
if (this.canvas.selectedIds.includes(id)) {
this.canvas.selectedIds = this.canvas.selectedIds.filter(i => i !== id)
} else {
this.canvas.selectedIds.push(id)
}
} else {
this.canvas.selectedIds = [id]
}
},
// 记录历史
recordHistory() {
this.history.past.push(cloneDeep(this.canvas.components))
this.history.future = []
// 限制历史记录数量
if (this.history.past.length > 50) {
this.history.past.shift()
}
},
// 撤销
undo() {
if (this.canUndo) {
const previous = this.history.past.pop()!
this.history.future.push(cloneDeep(this.canvas.components))
this.canvas.components = previous
}
},
// 重做
redo() {
if (this.canRedo) {
const next = this.history.future.pop()!
this.history.past.push(cloneDeep(this.canvas.components))
this.canvas.components = next
}
},
// 清空画布
clear() {
this.recordHistory()
this.canvas.components = []
this.canvas.selectedIds = []
},
// 导出配置
exportConfig() {
return {
version: '1.0',
components: this.canvas.components
}
},
// 导入配置
importConfig(config: any) {
this.recordHistory()
this.canvas.components = config.components || []
this.canvas.selectedIds = []
}
}
})
```bash
### 4.2 元数据状态 (metadata.ts)
```typescript
import { defineStore } from 'pinia'
import { metadataApi } from '@/api/metadata'
export const useMetadataStore = defineStore('metadata', {
state: () => ({
// 组件定义
components: [] as ComponentMeta[],
// 页面列表
pages: [] as PageMeta[],
// 数据模型
dataModels: [] as DataModelMeta[],
// 加载状态
loading: {
components: false,
pages: false,
dataModels: false
}
}),
getters: {
// 按分类获取组件
componentsByCategory: (state) => {
const map = new Map<string, ComponentMeta[]>()
state.components.forEach(component => {
const category = component.category || 'other'
if (!map.has(category)) {
map.set(category, [])
}
map.get(category)!.push(component)
})
return map
},
// 获取组件定义
getComponentMeta: (state) => (type: string) => {
return state.components.find(c => c.type === type)
}
},
actions: {
// 加载组件列表
async loadComponents() {
this.loading.components = true
try {
const res = await metadataApi.getComponents()
this.components = res.data.list
} finally {
this.loading.components = false
}
},
// 加载页面列表
async loadPages() {
this.loading.pages = true
try {
const res = await metadataApi.getPages()
this.pages = res.data.list
} finally {
this.loading.pages = false
}
},
// 加载数据模型
async loadDataModels() {
this.loading.dataModels = true
try {
const res = await metadataApi.getDataModels()
this.dataModels = res.data.list
} finally {
this.loading.dataModels = false
}
}
}
})4.3 运行时状态 (runtime.ts)
typescript
import { defineStore } from 'pinia'
export const useRuntimeStore = defineStore('runtime', {
state: () => ({
// 页面配置
pageConfig: null as PageConfig | null,
// 表单数据
formData: {} as Record<string, any>,
// 页面变量
variables: {} as Record<string, any>,
// 加载状态
loading: false,
// 提交状态
submitting: false
}),
actions: {
// 加载页面配置
async loadPage(pageId: string) {
this.loading = true
try {
const res = await api.getPage(pageId)
this.pageConfig = res.data
this.initFormData()
} finally {
this.loading = false
}
},
// 初始化表单数据
initFormData() {
if (!this.pageConfig) return
const data: Record<string, any> = {}
// 从配置中提取默认值
this.extractDefaultValues(this.pageConfig.components, data)
this.formData = data
},
// 提取默认值
extractDefaultValues(components: ComponentSchema[], data: Record<string, any>) {
components.forEach(component => {
if (component.props?.field && component.props?.defaultValue !== undefined) {
data[component.props.field] = component.props.defaultValue
}
if (component.children) {
this.extractDefaultValues(component.children, data)
}
})
},
// 更新表单数据
updateFormData(field: string, value: any) {
this.formData[field] = value
},
// 设置变量
setVariable(key: string, value: any) {
this.variables[key] = value
},
// 提交表单
async submitForm() {
this.submitting = true
try {
// 校验
const { valid, errors } = await validateForm(
this.pageConfig!.formSchema,
this.formData
)
if (!valid) {
ElMessage.error('表单校验失败')
return { success: false, errors }
}
// 提交数据
const res = await api.submitData(this.pageConfig!.model, this.formData)
ElMessage.success('提交成功')
return { success: true, data: res.data }
} finally {
this.submitting = false
}
}
}
})
```bash
---
## 五、组件体系
### 5.1 组件 Schema 定义
```typescript
// 组件元数据
interface ComponentMeta {
type: string // 组件类型标识
label: string // 组件名称
category: string // 分类
icon: string // 图标
schema: PropertySchema[] // 属性配置 schema
defaultProps: Record<string, any> // 默认属性
events: EventSchema[] // 事件定义
slots?: string[] // 插槽
preview?: string // 预览图
}
// 示例:输入框组件元数据
const inputComponentMeta: ComponentMeta = {
type: 'el-input',
label: '输入框',
category: 'form',
icon: 'Edit',
schema: [
{
label: '字段名',
name: 'field',
type: 'input',
required: true,
group: 'basic'
},
{
label: '标签',
name: 'label',
type: 'input',
group: 'basic'
},
{
label: '占位符',
name: 'placeholder',
type: 'input',
group: 'basic'
},
{
label: '默认值',
name: 'defaultValue',
type: 'input',
group: 'basic'
},
{
label: '宽度',
name: 'width',
type: 'input',
group: 'style'
}
],
defaultProps: {
placeholder: '请输入',
clearable: true
},
events: [
{ name: 'change', label: '值改变' },
{ name: 'blur', label: '失去焦点' },
{ name: 'focus', label: '获得焦点' }
]
}5.2 自定义组件包装
所有低代码组件需要包装一层,添加设计时功能:
vue
<!-- widgets/form/Input.vue -->
<template>
<div class="lc-input-wrapper" :class="{ 'is-design-mode': designMode }">
<el-input
v-model="modelValue"
v-bind="$attrs"
:placeholder="placeholder"
:disabled="computedDisabled"
:readonly="readonly"
:clearable="clearable"
@change="handleChange"
@blur="handleBlur"
@focus="handleFocus"
/>
<!-- 设计时遮罩 -->
<div v-if="designMode" class="design-mask" @click="handleSelect">
<div class="design-toolbar">
<el-button size="small" icon="Edit" circle />
<el-button size="small" icon="Delete" circle />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { evaluateExpression } from '@/engine/evaluator'
interface Props {
field?: string
label?: string
placeholder?: string
disabled?: string | boolean
readonly?: boolean
clearable?: boolean
defaultValue?: any
// 运行时上下文
context?: RenderContext
// 设计模式
designMode?: boolean
}
const props = withDefaults(defineProps<Props>(), {
clearable: true,
designMode: false
})
const emit = defineEmits(['change', 'blur', 'focus', 'select'])
// 计算禁用状态
const computedDisabled = computed(() => {
if (typeof props.disabled === 'boolean') {
return props.disabled
}
if (typeof props.disabled === 'string' && props.context) {
return evaluateExpression(props.disabled, props.context)
}
return false
})
// 值
const modelValue = computed({
get() {
return props.context?.data[props.field!] ?? props.defaultValue
},
set(value) {
if (props.field && props.context) {
props.context.data[props.field] = value
}
}
})
// 事件处理
const handleChange = (value: string) => {
emit('change', value)
}
const handleSelect = () => {
if (props.designMode) {
emit('select')
}
}
</script>
<style scoped>
.lc-input-wrapper {
position: relative;
}
.is-design-mode .design-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
cursor: pointer;
}
.design-mask:hover {
border: 2px solid #409eff;
}
.design-toolbar {
position: absolute;
top: -40px;
right: 0;
}
</style>
```bash
---
## 六、关键技术实现
### 6.1 拖拽实现 (useDrag.ts)
```typescript
import { ref } from 'vue'
export const useDrag = () => {
const isDragging = ref(false)
const dragData = ref<any>(null)
// 开始拖拽
const handleDragStart = (e: DragEvent, data: any) => {
isDragging.value = true
dragData.value = data
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'copy'
e.dataTransfer.setData('application/json', JSON.stringify(data))
}
}
// 拖拽结束
const handleDragEnd = () => {
isDragging.value = false
dragData.value = null
}
// 允许放置
const handleDragOver = (e: DragEvent) => {
e.preventDefault()
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'copy'
}
}
// 放置
const handleDrop = (e: DragEvent, callback: (data: any) => void) => {
e.preventDefault()
try {
const data = e.dataTransfer?.getData('application/json')
if (data) {
const parsed = JSON.parse(data)
callback(parsed)
}
} catch (error) {
console.error('Drop failed:', error)
}
}
return {
isDragging,
dragData,
handleDragStart,
handleDragEnd,
handleDragOver,
handleDrop
}
}6.2 快捷键支持 (useKeyboard.ts)
typescript
import { onMounted, onUnmounted } from 'vue'
export const useKeyboard = () => {
const keyHandlers = new Map<string, Function>()
// 注册快捷键
const register = (key: string, handler: Function) => {
keyHandlers.set(key, handler)
}
// 处理按键
const handleKeyDown = (e: KeyboardEvent) => {
const key = getKeyString(e)
const handler = keyHandlers.get(key)
if (handler) {
e.preventDefault()
handler()
}
}
// 获取按键字符串
const getKeyString = (e: KeyboardEvent): string => {
const parts: string[] = []
if (e.ctrlKey || e.metaKey) parts.push('Ctrl')
if (e.shiftKey) parts.push('Shift')
if (e.altKey) parts.push('Alt')
parts.push(e.key.toUpperCase())
return parts.join('+')
}
onMounted(() => {
window.addEventListener('keydown', handleKeyDown)
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown)
})
return { register }
}
// 使用示例
const { register } = useKeyboard()
register('Ctrl+S', () => {
// 保存
savePage()
})
register('Ctrl+Z', () => {
// 撤销
designerStore.undo()
})
register('Ctrl+Y', () => {
// 重做
designerStore.redo()
})
register('Delete', () => {
// 删除选中组件
deleteSelected()
})
```bash
### 6.3 性能优化
#### 1. 虚拟滚动(大列表优化)
```vue
<template>
<virtual-list
:data-key="'id'"
:data-sources="components"
:data-component="ComponentItem"
:keeps="30"
/>
</template>
<script setup>
import VirtualList from 'vue-virtual-scroll-list'
import ComponentItem from './ComponentItem.vue'
</script>2. 组件懒加载
typescript
// router/index.ts
const routes = [
{
path: '/designer/page',
component: () => import('@/views/designer/PageDesigner.vue')
}
]
```bash
#### 3. 图片懒加载
```vue
<template>
<img v-lazy="imageSrc" alt="preview" />
</template>七、配置文件
7.1 Vite 配置 (vite.config.ts)
typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { resolve } from 'path'
import monacoEditorPlugin from 'vite-plugin-monaco-editor'
export default defineConfig({
plugins: [
vue(),
vueJsx(),
monacoEditorPlugin({})
],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
},
build: {
rollupOptions: {
output: {
manualChunks: {
'element-plus': ['element-plus'],
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'charts': ['echarts', 'vue-echarts']
}
}
}
}
})
```bash
---
**文档版本:** v1.0
**最后更新:** 2024-01-01