Files
fauto-design/项目设计文档.md
2026-01-20 20:25:03 +08:00

12 KiB
Raw Blame History

Vue页面可视化设计器 - 项目设计文档

项目概述

本项目是一个基于 Vite + Vue3 + TypeScript + Element Plus 的可视化页面设计器通过拖拽操作直接编辑真实的Vue源文件实现低代码页面快速构建。

技术架构

技术栈

类别 技术 版本
构建工具 Vite 7.3.0
前端框架 Vue 3 (Composition API) 3.5.24
UI组件库 Element Plus -
状态管理 Pinia 3.0.4
类型系统 TypeScript -
后端运行时 Node.js 22.x
后端框架 Express -
模板解析 @vue/compiler-sfc, @vue/compiler-dom -

系统架构

┌─────────────────────────────────────────────────────────────┐
│                     前端 (Vue3 + Vite)                       │
├─────────────────────────────────────────────────────────────┤
│  Designer.vue                                               │
│  ├── Header (顶部菜单)                                       │
│  ├── MainLayout (三栏布局)                                   │
│  │   ├── 左侧面板 (PageManager, DesignComponentList)         │
│  │   ├── 中间面板 (DesignCenter + InteractiveWrapper)        │
│  │   └── 右侧面板 (属性编辑器 - 待实现)                        │
│  └── Footer (状态栏 - 显示拖放记录)                           │
├─────────────────────────────────────────────────────────────┤
│  状态管理层                                                   │
│  ├── dragStore.ts    (拖拽状态、两阶段逻辑、层级选择)           │
│  ├── panelStore.ts   (面板布局状态)                           │
│  ├── designStore.ts  (设计组件元数据)                         │
│  └── vueFileStore.ts (Vue文件选择状态)                        │
└─────────────────────────────────────────────────────────────┘
                              │
                              │ HTTP API
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                  后端 (Node.js + Express)                    │
├─────────────────────────────────────────────────────────────┤
│  API: POST /api/move-element                                 │
│  ├── 接收: { pagePath, source, target, direction }          │
│  └── 处理: templateService.moveElement()                     │
├─────────────────────────────────────────────────────────────┤
│  templateService.js                                          │
│  ├── parseSFC() - 解析Vue单文件组件                          │
│  ├── parseTemplate() - 解析template获取AST                   │
│  ├── findElementByPath() - 根据路径定位元素                   │
│  ├── moveElement() - 移动元素(同级前后)                       │
│  └── moveElementInside() - 移动元素(放入内部)                 │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Vue源文件 (src/views/)                    │
└─────────────────────────────────────────────────────────────┘

核心功能模块

1. 拖拽状态管理 (dragStore.ts)

核心状态

// 拖拽阶段
dragPhase: 'source' | 'target'

// 拖拽源信息
dragSource: {
  type: 'design-component' | 'canvas-element'
  componentId?: string
  componentName?: string
  path?: string
  elementType?: 'er' | 'ec'
}

// 已确定的源元素(进入目标阶段时保存)
confirmedSource: {
  path: string
  type: 'er' | 'ec'
  element: HTMLElement
}

// 层级节点列表(从深到浅排序)
hierarchyNodes: HierarchyNode[]

// 当前选中的层级索引
selectedHierarchyIndex: number

// 拖放方向
hoverDirection: 'top' | 'bottom' | 'left' | 'right' | 'inside' | null

两阶段拖拽流程

源选择阶段 (source)
├── 触发: startDragFromCanvas(path, type, element)
├── 功能: 
│   ├── 构建当前元素的层级列表
│   └── ↑↓键切换要拖拽的源层级
├── 特点:
│   ├── 不显示DropZone
│   └── 松开鼠标取消拖拽
└── 退出: 移动到其他元素 → enterTargetPhase()

目标选择阶段 (target)
├── 触发: enterTargetPhase(targetElement)
├── 功能:
│   ├── 保存确定的源元素到 confirmedSource
│   ├── 构建目标元素的层级列表
│   └── ↑↓键切换目标层级
├── 特点:
│   ├── 显示DropZone
│   └── 根据源/目标类型显示方向
└── 退出: 松开鼠标 → confirmDrop()

跨类型拖放逻辑

const isCrossTypeDrop = computed(() => {
  if (dragPhase.value !== 'target') return false
  if (!confirmedSource.value || !selectedNode.value) return false
  
  // 源类型与目标类型不同
  return confirmedSource.value.type !== selectedNode.value.type
})

// 跨类型时显示 'inside' 方向
if (isCrossTypeDrop.value) {
  hoverDirection.value = 'inside'
}

2. 交互注入器 (InteractiveWrapper.vue)

核心职责

  1. 动态渲染Vue页面 - 使用 defineAsyncComponent 加载选中的页面
  2. 注入交互事件 - 为所有 el-row/el-col 绑定事件
  3. 监听DOM变化 - 使用 MutationObserver 检测新元素
  4. 键盘事件处理 - ↑↓切换层级Esc取消

事件绑定

const bindElementEvents = (element, type, path) => {
  // 标记已绑定
  element.setAttribute('data-fauto-bindend', 'true')
  element.setAttribute('data-path', path)
  element.classList.add('fauto-interactive')
  
  // 悬停事件 - 进入目标阶段
  element.addEventListener('mouseenter', (e) => {
    if (dragStore.isDragging) {
      const sourcePath = dragStore.confirmedSource?.path || dragStore.dragSource?.path
      if (sourcePath && path !== sourcePath && !path.startsWith(sourcePath)) {
        dragStore.enterTargetPhase(element)
      }
    }
  })
  
  // 按下事件 - 开始拖拽
  element.addEventListener('mousedown', (e) => {
    if (!dragStore.isDragging) {
      dragStore.startDragFromCanvas(path, type, element)
    }
  })
}

全局事件处理

// 全局鼠标移动 - 更新拖放方向
const handleGlobalMouseMove = (e) => {
  if (dragStore.isDragging && dragStore.dragPhase === 'target') {
    dragStore.updateDirectionFromMouse(e.clientX, e.clientY)
  }
}

// 全局鼠标松开 - 确认拖放
const handleGlobalMouseUp = async () => {
  if (dragStore.isDragging && dragStore.dragPhase === 'target') {
    await dragStore.confirmDrop(vueFileStore.selectedFilePath)
  }
  dragStore.endDrag()
}

3. 后端模板服务 (templateService.js)

路径解析

// 解析路径: "r1c2r1" → [{type:'r',index:1}, {type:'c',index:2}, {type:'r',index:1}]
function parsePath(pathStr) {
  const nodes = []
  const regex = /([rc])(\d+)/g
  let match
  while ((match = regex.exec(pathStr)) !== null) {
    nodes.push({ type: match[1], index: parseInt(match[2], 10) })
  }
  return nodes
}

元素定位

function findElementByPath(ast, pathStr) {
  const pathNodes = parsePath(pathStr)
  let currentChildren = ast.children
  let currentNode = null
  
  for (const pathNode of pathNodes) {
    const targetType = pathNode.type === 'r' ? 'el-row' : 'el-col'
    let count = 0
    
    for (const child of currentChildren) {
      if (child.type === 1 && child.tag === targetType) {
        count++
        if (count === pathNode.index) {
          currentNode = child
          currentChildren = child.children || []
          break
        }
      }
    }
  }
  return currentNode
}

缩进调整算法

function adjustIndentation(sourceText, targetIndent) {
  const lines = sourceText.split('\n')
  
  // 获取第一行的原始缩进
  const firstLineIndent = getIndentLevel(lines[0])
  
  // 计算缩进差值
  const indentDiff = targetIndent.length - firstLineIndent
  
  // 对每一行应用缩进差值(保持相对缩进)
  return lines.map(line => {
    if (!line.trim()) return ''
    const currentIndent = getIndentLevel(line)
    const newIndent = Math.max(0, currentIndent + indentDiff)
    return ' '.repeat(newIndent) + line.trimStart()
  }).join('\n')
}

元素移动(从后向前处理)

// 关键:从后向前处理,避免偏移量错误
if (deleteStart > insertPosition) {
  // 删除位置在后:先删除,再插入
  result = content.substring(0, deleteStart) + content.substring(deleteEnd)
  result = result.substring(0, insertPosition) + insertText + result.substring(insertPosition)
} else {
  // 删除位置在前:先删除,调整插入位置
  const deletedLength = deleteEnd - deleteStart
  const adjustedInsertPos = insertPosition - deletedLength
  result = content.substring(0, deleteStart) + content.substring(deleteEnd)
  result = result.substring(0, adjustedInsertPos) + insertText + result.substring(adjustedInsertPos)
}

开发规范

Vue页面结构规范

<template>
  <!-- 第一层级必须且只能有一个 el-row -->
  <el-row class="page-container" :gutter="20">
    <!-- el-row 内只能包含 el-col -->
    <el-col :span="12">
      <!-- el-col 内可以放设计组件或嵌套 el-row -->
      <div class="design-component">内容</div>
    </el-col>
    <el-col :span="12">
      <el-row :gutter="10">
        <el-col :span="24">嵌套内容</el-col>
      </el-row>
    </el-col>
  </el-row>
</template>

代码规范

  1. 使用 TypeScript 严格模式
  2. 组件使用 Composition API
  3. Props 必须明确定义类型
  4. 样式使用 scoped 避免污染

插件开发规范

所有与页面交互相关的代码放在 src/fauto/plugins/ 目录:

  • 状态管理类使用 Pinia defineStore
  • 工具函数单独文件导出
  • 统一通过 index.ts 导出

API 接口

移动元素

POST /api/move-element

Request:
{
  "pagePath": "views/TestPage1.vue",  // 相对于 src 的路径
  "source": "r1c1",                    // 源元素路径
  "target": "r1c2",                    // 目标元素路径
  "direction": "right"                 // top|bottom|left|right|inside
}

Response:
{
  "success": true,
  "message": "元素移动成功"
}

待实现功能

  1. 属性编辑器 - 展示和编辑选中组件的属性
  2. 撤销/重做 - 操作历史记录
  3. 设计组件拖入 - 从组件列表拖入新组件
  4. 预览模式 - 隐藏交互层预览最终效果
  5. 导出功能 - 导出设计结果

已知问题

  1. 拖放操作有概率生成错误HTML结构已基本修复
  2. 复杂嵌套结构下缩进可能不完美

最后更新2026-01-20