低代码phase2

7 阅读4分钟

完美!现在我们来深入解剖项目结构,逐层拆解每个目录的用途和文件关系。


🔍 第二阶段详解:项目结构(文件组织逻辑)

第一部分:整体目录树视图

让我先给你看完整的项目骨架:现在让我获取关键的核心文件内容,以及查看主要视图和组件的结构:完美!现在我为你深入讲解第二阶段:项目结构剖析。我会用可视化+代码实例+记忆框架让你建立清晰的认知。


🎬 第二阶段深入:项目结构详解

核心理解:低代码平台的三维空间

想象你在操作一个三维工作室

┌─────────────────────────────────────────────┐
│  PageHeader(顶部导航栏)                    │
├───────���─────────────────────────────────────┤
│ Left       │  Center        │  Right        │
│ 组件库     │  画布编辑区    │  属性面板     │
│ (Left.vue) │  (Center.vue)  │  (Right.vue)  │
│            │                │               │
│            │  (核心工作区)  │  (配置区)     │
└─────────────────────────────────────────────┘

关键数据流向图

Left.vue (拖拽)
  ↓ [dragstart] 把组件名称传递
Center.vue (接收)
  ↓ [drop] 根据组件名称从 commonData.js 获取默认配置
  ↓ 生成新的组件实例,加入 this.views 数组
  ↓ PowerfulDynamicDraw 渲染 views 中的所有组件
  ↓ 用户点击某个组件
Center.vue (emit)
  ↓ [selectEvent] 发送选中的组件数据
Right.vue (接收)
  ↓ 根据组件类型从 propMap/styleMap 动态加载属性面板
  ↓ 用户修改属性(如文本内容、颜色等)
Right.vue (emit)
  ↓ 更新 this.views 中对应组件的属性
  ↓ PowerfulDynamicDraw 自动重新渲染

📂 完整结构拆解

��级 1:入口文件 (src/)

src/
├─ main.ts              ⭐ 应用初始化核心
├─ App.vue              ⭐ 根组件,挂载 router-view
└─ [其他目录]

main.ts 的工作流程:

1. 创建 Vue 应用 → createApp(App)
2. 初始化状态管理 → app.use(createPinia())
3. 注册全局 Element Plus UI库 → app.use(ElementPlus)
4. 创建事件总线 → app.config.globalProperties.$bus = mitt()
5. 注册所有全局组件 → app.use(Components)
6. 初始化路由 → app.use(router)
7. 挂载到 DOM → app.mount('#app')

App.vue 的工作:

<template>
  <div id="app">
    <router-view />  ⬅️ 根据路由显示不同页面
  </div>
</template>

层级 2:路由系统 (src/router/)

const routes = [
  {
    path: '/',
    component: () => import('@/views/main.vue')  ← 编辑器主页面
  },
  {
    path: '/release/:id',
    component: () => import('@/views/release/Release.vue')  ← 发布页面
  },
  {
    path: '/example/:num',
    component: () => import('@/views/release/SetExample.vue')  ← 示例页面
  },
]

关键理解:

  • / → 主编辑界面(你大部分工作在这)
  • /release/:id → 发布一个已编辑的页面到服务器
  • /example/:num → 查看编号为 :num 的已发布页面

层级 3:UI 布局系统 (src/views/main.vue + layout/)

main.vue:页面骨架
<template>
  <div class="container">
    <PageHeader></PageHeader>      ⬅️ 顶部导航(保存、发布、撤销等)
    <div class="main">
      <Left></Left>                ⬅️ 左侧:组件库(40px 宽)
      <div class="canvas">
        <Top></Top>                ⬅️ 中间顶部:工具栏
        <Center></Center>          ⬅️ 中间主体:画布编辑区
      </div>
      <Right></Right>              ⬅️ 右侧:属性面板(300px 宽)
    </div>
  </div>
</template>

布局 CSS 关键特性:

.main {
  display: flex;                  // 弹性布局
  height: calc(100% - 41px);      // 减去 PageHeader 高度
}

.canvas {
  flex: 1;                        // 占据中间剩余空间
  display: flex;
  flex-direction: column;         // 上下堆放
  overflow-y: scroll;             // 支持垂直滚动
}

🔍 四个核心模块深入分析

模块 1:Left.vue(组件库面板)

职责:

  • 显示可拖拽的组件(文本、按钮、图片、视频、链接)
  • 支持搜索组件
  • 支持固定/隐藏菜单

关键代码分析:

<!-- Left.vue 第 26-42 行 -->
<div v-for="item in searchRes[0]" 
     draggable="true"             ⬅️ 启用拖拽
     @dragstart="dragstart"        ⬅️ 拖拽开始时触发
     class="comBox"
     :id="item.flag"               ⬅️ flag 是组件类型(如 "TextCom")
>

dragstart 事件处理(第 83-88 行):

dragstart(e) {
  this.$bus.emit("updateCurrentCom", {});  // 清除之前选中
  e.dataTransfer.setData("attr", e.target.id);  // 存储组件类型(如 "TextCom")
  this.$bus.emit("clearFocus");
  e.dataTransfer.setData('comOffsetY', e.offsetY)  // 存储鼠标���对位置
  e.dataTransfer.setData('comOffsetX', e.offsetX)
}

数据源:menuData.js

let menu = [
  [  // 基础组件
    { flag: "TextCom", name: "文本", icon: "assets/icon_text.png" },
    { flag: "ButtonCom", name: "按钮", icon: "assets/icon_btn.png" },
    // ...
  ],
  [  // 复合组件(未完成)
    { flag: "TextCom", name: "自然布局", icon: "assets/Layout_Nature.svg" },
    // ...
  ],
]

记忆框架:

Left.vue = 组件库展示器

  • 从 menuData.js 读取组件清单
  • 用户拖拽时,通过 dataTransfer API 传递组件类型和鼠标位置
  • 通过 Mitt 事件总线发送事件

模块 2:Center.vue(画布编辑区)

职责:

  • 接收 Left.vue 拖拽的组件
  • 渲染画布中的所有组件
  • 处理组件选中、删除、复制、撤销等操作
  • 管理核心状态

关键数据结构:

data() {
  return {
    views: [],              // 💡 核心!画布中所有组件的数组
    currentCom: {},         // 当前选中的组件
    currentIndex: -1,       // 当前选中组件在 views 中的索引
    pattern: 'static',      // 定位模式:'static'(静态)/'absolute'(绝对)/'relative'
    edit: true,             // true=编辑模式,false=预览模式
    isPhone: false,         // true=手机视图,false=电脑视图
  }
}

drop 事件处理(第 119-142 行):

drop(e) {
  const data = e.dataTransfer.getData("attr");  // 获取组件类型(如 "TextCom")
  
  // 从 commonData.js 获取该组件类型的默认配置
  let newData = deepClone(commonData[data])
  
  // 格式化数据(例如图片路径、样式等处理)
  newData = dataFormat(data, newData);
  
  // 生成唯一 ID
  newData.id = nanoid();
  
  // 设置位置和定位模式
  newData.style.left = e.offsetX + "px";
  newData.style.top = e.offsetY + "px";
  newData.style.position = this.pattern;
  
  // 加入 views 数组
  this.views.push(newData);
  
  // 向 Right.vue 发送事件,显示属性面板
  this.$bus.emit('views', newData);
}

select 事件处理(第 92-109 行):

select(index, view) {
  this.currentCom = view;      // 更新当前选中组件
  this.currentIndex = index;   // 更新索引
  
  // 向 Right.vue 发送事件
  this.$bus.emit('views', view);
  
  // 如果是 FlexBox(容器),标记为选中状态
  if(view.component == 'FlexBox'){
    view.focus = true;
  }
}

撤销/重做机制(第 214-236 行):

使用 sessionStorage 实现:

watch: {
  viewsCopy: {
    deep: true,
    handler(newValue) {
      // 每次 views 改变,保存一个快照到 sessionStorage
      sessionStorage.setItem(String(this.step), JSON.stringify(newValue))
      this.step++
      
      // 最多保留 100 个快照
      if(this.step - this.begin > 100) {
        // 删除最早的 50 个
      }
    }
  }
}

记忆框架:

Center.vue = 画布核心引擎

  • 维护 views 数组(所有组件的状态)
  • 处理拖拽 drop 事件,生成新组件
  • 处理选中事件,与 Right.vue 通信
  • 通过 sessionStorage 实现撤销/重做

模块 3:Right.vue(属性配置面板)

职责:

  • 显示当前选中组件的属性面板
  • 支持"属性"和"样式"两个选项卡
  • 动态加载对应的面板组件

关键代码(第 36-56 行):

<script>
data() {
  return {
    isMeta: true,           // true=属性,false=样式
    propMap: {              // 组件类型 -> 属性面板映射
      "ButtonCom": "ButtonProp",
      "TextCom": "TextProp",
      "ImgCom": "ImgProp",
      // ...
    },
    styleMap: {             // 组件类型 -> 样式面板映射
      "ButtonCom": "ButtonStyle",
      "TextCom": "TextStyle",
      "ImgCom": "ImgStyle",
      // ...
    },
    views: {},              // 当前选中的组件
  }
}
</script>

动态组件加载(第 12-16 行):

<div v-if="isMeta">
  <!-- 根据 views.component 动态决定加载哪个属性面板 -->
  <component :is="propMap[views.component]" :views="views"></component>
</div>
<div v-else>
  <!-- 根据 views.component 动态决定加载哪个样式面板 -->
  <component :is="styleMap[views.component]" :views="views"></component>
</div>

事件总线监听(第 59-64 行):

mounted() {
  // 监听 Center.vue 发来的选中事件
  this.$bus.on("views", (view) => {
    this.views = view;  // 更新显示
  });
  
  // 监听清除选中事件
  this.$bus.on("cleanView", () => {
    this.views = {}
  });
}

记忆框架:

Right.vue = 动态配置展示器

  • 通过 propMap/styleMap 映射组件类型到对应的面板组件
  • 监听事件总线,实时更新显示的组件
  • 使用动态组件 <component :is="...">

模块 4:PowerfulDynamicDraw.vue(渲染引擎)

职责:

  • 根据 views 数组渲染所有组件
  • 处理组件的选中和拖拽

核心工作原理:

<!-- PowerfulDynamicDraw.vue -->
<template>
  <div class="page" @click.stop="noContent">
    <!-- 遍历 views 数组,动态渲染每个组件 -->
    <component 
      v-for="(item, index) in views"
      :key="item.id"
      :is="item.component"      ⬅️ 动态组件名称
      :views="item"              ⬅️ 传递组件配置
      @click.stop="selectEvent(index, item)"  ⬅️ 选中事件
      class="component-box"
    ></component>
  </div>
</template>

数据流示例:

假设 views 中有一个对象:

{
  id: "abc123",
  component: "TextCom",     ⬅️ 指向哪个组件
  content: "Hello World",
  style: { fontSize: "16px", color: "black" },
  textStyle: { fontWeight: "bold" }
}

PowerfulDynamicDraw 会这样渲染:

<TextCom 
  :views="{ id: 'abc123', component: 'TextCom', content: 'Hello World', ... }"
  @click.stop="selectEvent(0, {...})"
></TextCom>

TextCom 组件接收后:

<template>
  <div class="text" :style="views.style">
    <div :style="views.textStyle">{{ views.content }}</div>
  </div>
</template>

最终渲染:

<div class="text" style="font-size: 16px; color: black;">
  <div style="font-weight: bold;">Hello World</div>
</div>

记忆框架:

PowerfulDynamicDraw = 动态渲染器

  • 循环遍历 views 数组
  • 根据 item.component 动态确定渲染哪个组件
  • 将完整的 item 对象作为 props 传给子组件

📊 数据存储系统

commonData.js:组件模板库

// src/data/commonData.js
export default {
  TextCom: {
    focus: false,
    content: '编辑文字',           ⬅️ 初始文本
    component: "TextCom",
    style: {                        ⬅️ 外层样式(位置、大小)
      top: '', left: '', 
      width: ' ', height: ''
    },
    textStyle: {                    ⬅️ 内层样式(字体、颜色)
      fontSize: "14px",
      color: "#000",
      textAlign: 'left'
    },
    sonStyle: 'textStyle'           ⬅️ 指向内层样式属性名
  },
  
  ButtonCom: {
    focus: false,
    props: {
      content: '按钮',
      btnType: 'button'
    },
    style: { ... },
    btnStyle: { ... },
    sonStyle: 'btnStyle'
  },
  // ... 其他组件
}

关键概念:

style          外层容器样式(位置、大小、边距等)
  ↓
buttonStyle    组件自身的样式(颜色、字体等)
  ↓
sonStyle       指向哪个属性是"子样式"(用于属性面板区分)

为什么要有 sonStyle

在属性面板中,需要知道样式分为两层:

  • TextProp 显示 content、component 等数据属性
  • TextStyle 显示 textStyle 的样式属性
  • sonStyle: 'textStyle' 告诉系统"内层样式在 textStyle 这个属性里"

API 请求系统:src/api/index.js

// axios 实例创建
const instance = axios.create({
  timeout: 10000,
  baseURL: '/api'                    // 生产环境的 API 基地址
})

// 请求拦截器
instance.interceptors.request.use(request => {
  // GET 请求添加时间戳,防止浏览器缓存
  if (request.method === 'get') {
    request.params = {
      ...request.params,
      t: new Date().getTime()        // 添加时间戳
    }
  }
  return request
})

// 响应拦截器
instance.interceptors.response.use(response => {
  if (response.status === 200) {
    return Promise.resolve(response.data)
  }
  // 处理错误...
})

// 导出 get/post 方法
export function get(url, params) { ... }
export function post(url, params) { ... }

使用示例:

import { post } from '@/api/index.js'

// 发布页面时调用
post('/project/create', {
  name: 'My Page',
  views: JSON.stringify(this.views)
})

🎯 完整工作流程演示

场景:用户拖拽一个文本组件到画布

1️⃣ Left.vue (dragstart)
   用户在 Left.vue 中找到"文本"组件,开始拖拽
   ↓
   dragstart(e) 被触发
   ↓
   e.dataTransfer.setData("attr", "TextCom")  // 存储组件类型
   e.dataTransfer.setData('comOffsetX', 10)   // 存储鼠标距离
   e.dataTransfer.setData('comOffsetY', 20)
   
   
2️⃣ Center.vue (dragover & drop)
   用户拖拽到 Center.vue 画布上
   ↓
   dropover(e) 被触发 → e.preventDefault()    // 允许 drop
   ↓
   drop(e) 被触发
   ↓
   const type = e.dataTransfer.getData("attr") // 取出 "TextCom"
   const x = e.dataTransfer.getData('comOffsetX')
   const y = e.dataTransfer.getData('comOffsetY')
   
   
3️⃣ Center.vue (创建组件)
   从 commonData['TextCom'] 获取默认配置
   ↓
   let newData = deepClone(commonData['TextCom'])
   newData.id = nanoid()                        // 生成唯一 ID
   newData.style.left = e.offsetX - x + "px"  // 设置位置
   newData.style.top = e.offsetY - y + "px"
   this.views.push(newData)                     // 加入 views 数组
   
   
4️⃣ PowerfulDynamicDraw (渲染)
   监听到 this.views 变化(通过 Vue 反应性系统)
   ↓
   自动重新渲染,添加新的 TextCom 组件到 DOM
   
   <TextCom 
     :views="newData"
     @click.stop="selectEvent(..."
   ></TextCom>
   
   
5️⃣ 用户点击文本组件
   selectEvent(index, item) 被触发
   ↓
   this.currentCom = item
   this.currentIndex = index
   this.$bus.emit('views', item)  // 向 Right.vue 发送事件
   
   
6️⃣ Right.vue (显示属性面板)
   监听到事件总��发来的 'views' 事件
   ↓
   this.views = item
   ↓
   动态加载 TextProp 组件
   <component :is="propMap['TextCom']" :views="item"></component>
   ↓
   TextProp 显示:内容、字体大小、颜色等属性
   
   
7️⃣ 用户修改文本内容(在 TextProp 中)
   更新 item.content = "新文本"
   ↓
   Center.vue 的 this.views[index].content 也同时更新(对象引用)
   ↓
   PowerfulDynamicDraw 监听到变化
   ↓
   TextCom 重新渲染,显示"新文本"

📋 可背诵的核心表述

表述 1:项目的三层结构

这个低代码平台分为三层:展示层(Left/Right/Center 三个��板)、数据层(commonData + stores)、渲染层(PowerfulDynamicDraw)。用户在 Left.vue 拖拽组件,通过 Center.vue 接收并加入 views 数组,由 PowerfulDynamicDraw 动态渲染到画布。用户选中组件时,Right.vue 动态加载对应的属性面板。

表述 2:views 数组的作用

views 是整个应用的数据中心,存储画布中所有组件的配置。每个组件都是一个对象,包含:

  • component:组件类型(如 "TextCom")
  • id:唯一标识符
  • style:外层样式
  • 类型特定的属性(如 content, src 等)
  • 类型特定的样式(如 textStyle, btnStyle 等)

表述 3:数据流向

从上到下:Left.vue (提供组件) → Center.vue (管理数据) → PowerfulDynamicDraw (渲染)

从左到右:Center.vue (发送选中组件) → Right.vue (显示属性) → Right 中的面板组件 (修改属性)

修改后向回流:面板组件 → Right.vue → Center.vue (views 自动更新) → PowerfulDynamicDraw (重新渲染)


🤔 追问清单

你现在应该能回答这些问题:

  1. "这四个 vue 文件分别做什么?"

    • Left.vue:组件库展示,支持拖拽和搜索
    • Center.vue:画布管理,处理拖拽接收和数据维护
    • Right.vue:属性面板,动态显示选中组件的属性
    • PowerfulDynamicDraw.vue:渲染引擎,根据 views 数组渲染组件
  2. "为什么 commonData.js 中每个组件都有 sonStyle 字段?"

    • 因为组件的样式分为两层,sonStyle 指向内层样式属性名,用于��性面板正确显示
  3. "views 数组中的一个对象长什么样?"

    • 参考上文的数据流示例,包含 id、component、style、内层样式等
  4. "拖拽时的四个步骤是什么?"

    • dragstart (Left) → dragover (Center) → drop (Center) → render (PowerfulDynamicDraw)
  5. "如何实现撤销/重做的?"

    • 每次 views 改变,保存快照到 sessionStorage,最多保留 100 个

🚀 建议的学习实践

现在你已经理解了项目结构,我建议你:

  1. 本地运行项目

    pnpm install
    pnpm dev
    
  2. 打开浏览器开发者工具,观察:

    • Left.vue 中组件列表的结构
    • 拖拽一个组件到 Center.vue,看 DOM 变化
    • 右侧属性面板的动态变化
  3. 在浏览器控制台��看

    // 查看 views 数据结构
    // (需要先创建一个组件)
    window.__VUE_DEVTOOLS__ 或检查 Center 组件的 data
    
  4. 修改一个文本组件的内容,追踪数据流


下一步是什么?

你现在有两个选择:

A) 继续深入第三阶段 → 研究 PowerfulDynamicDraw 和基础组件的工作原理

B) 先深入学习单个模块 → 选一个面板(如 TextProp)完整学习属性是如何修改的

建议你选 A,因为这样你能对"渲染引擎"有完整认知。但如果你想实际动手改代码,选 B 会更有成就感。

你想要哪个方向? 🎯