完美!现在我们来深入解剖项目结构,逐层拆解每个目录的用途和文件关系。
🔍 第二阶段详解:项目结构(文件组织逻辑)
第一部分:整体目录树视图
让我先给你看完整的项目骨架:现在让我获取关键的核心文件内容,以及查看主要视图和组件的结构:完美!现在我为你深入讲解第二阶段:项目结构剖析。我会用可视化+代码实例+记忆框架让你建立清晰的认知。
🎬 第二阶段深入:项目结构详解
核心理解:低代码平台的三维空间
想象你在操作一个三维工作室:
┌─────────────────────────────────────────────┐
│ 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 (重新渲染)
🤔 追问清单
你现在应该能回答这些问题:
-
"这四个 vue 文件分别做什么?"
- Left.vue:组件库展示,支持拖拽和搜索
- Center.vue:画布管理,处理拖拽接收和数据维护
- Right.vue:属性面板,动态显示选中组件的属性
- PowerfulDynamicDraw.vue:渲染引擎,根据 views 数组渲染组件
-
"为什么 commonData.js 中每个组件都有
sonStyle字段?"- 因为组件的样式分为两层,sonStyle 指向内层样式属性名,用于��性面板正确显示
-
"views 数组中的一个对象长什么样?"
- 参考上文的数据流示例,包含 id、component、style、内层样式等
-
"拖拽时的四个步骤是什么?"
- dragstart (Left) → dragover (Center) → drop (Center) → render (PowerfulDynamicDraw)
-
"如何实现撤销/重做的?"
- 每次 views 改变,保存快照到 sessionStorage,最多保留 100 个
🚀 建议的学习实践
现在你已经理解了项目结构,我建议你:
-
本地运行项目
pnpm install pnpm dev -
打开浏览器开发者工具,观察:
- Left.vue 中组件列表的结构
- 拖拽一个组件到 Center.vue,看 DOM 变化
- 右侧属性面板的动态变化
-
在浏览器控制台��看:
// 查看 views 数据结构 // (需要先创建一个组件) window.__VUE_DEVTOOLS__ 或检查 Center 组件的 data -
修改一个文本组件的内容,追踪数据流
下一步是什么?
你现在有两个选择:
A) 继续深入第三阶段 → 研究 PowerfulDynamicDraw 和基础组件的工作原理
B) 先深入学习单个模块 → 选一个面板(如 TextProp)完整学习属性是如何修改的
建议你选 A,因为这样你能对"渲染引擎"有完整认知。但如果你想实际动手改代码,选 B 会更有成就感。
你想要哪个方向? 🎯