Skip to content

低代码平台详细技术方案 - 前端架构 ​

基于 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.md

1.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