Files
yys-editor/docs/2design/ComponentArchitecture.md
rookie4show 15bae3be81 feat: 完成组件化改造 - 支持作为可嵌入组件使用
- 创建 YysEditorEmbed.vue 嵌入式组件
- 实现 preview/edit 双模式
- 配置 Vite library mode 构建
- 生成 ES Module + UMD + CSS 构建产物
- 完善设计文档和使用文档
- 更新 plan.md 标记阶段 2 完成

构建产物:
- dist/yys-editor.es.js (155KB, gzip: 35KB)
- dist/yys-editor.umd.js (112KB, gzip: 31KB)
- dist/yys-editor.css (69KB, gzip: 33KB)

相关文档:
- docs/2design/ComponentArchitecture.md
- docs/3build/YysEditorEmbed.md
- docs/3build/EMBED_README.md
- docs/4test/BUILD_TEST_REPORT.md
2026-02-20 17:23:59 +08:00

643 lines
16 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 组件化改造设计文档
## 背景与目标
### 为什么需要组件化改造
**当前状态**
- yys-editor 是一个独立的单页应用SPA
- 只能作为独立应用运行
- 无法嵌入到其他项目中
**目标**
- 将 yys-editor 改造为可嵌入的 Vue 组件
- 支持在 onmyoji-wiki 中作为块插件使用
- 保持独立应用功能不变(双重角色)
### 预期效果
**角色 1独立编辑器**(保持不变)
- 完整的流程图编辑应用
- 支持本地运行和使用
- 完整的 UI工具栏、组件库、属性面板
**角色 2可嵌入组件**(新增)
- 作为 onmyoji-wiki 的块插件
- 支持预览/编辑模式
- 轻量级,只包含核心编辑功能
- 数据接口清晰
---
## 技术方案
### 核心思路
**不修改现有代码,创建独立的嵌入式组件**
```
yys-editor/
├── src/
│ ├── App.vue # 独立应用(保持不变)
│ ├── YysEditorEmbed.vue # 嵌入式组件(新增)⭐
│ ├── components/ # 共享组件
│ │ ├── flow/
│ │ │ ├── FlowEditor.vue # 画布核心(共享)
│ │ │ ├── PropertyPanel.vue # 属性面板(共享)
│ │ │ └── ComponentsPanel.vue # 组件库(共享)
│ │ └── Toolbar.vue # 工具栏(共享)
│ └── ts/
│ ├── useStore.ts # 状态管理(共享)
│ └── useLogicFlow.ts # LogicFlow 封装(共享)
└── dist/ # 构建输出
├── yys-editor.es.js # ES Module库模式
└── yys-editor.umd.js # UMD库模式
```
### 架构设计
#### 1. 双模式架构
```
┌─────────────────────────────────────────────────────────┐
│ yys-editor 项目 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 独立应用模式 │ │ 嵌入组件模式 │ │
│ │ (App.vue) │ │ (YysEditorEmbed) │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ │ │
│ └────────────┬───────────────┘ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ 共享核心组件 │ │
│ ├─────────────────┤ │
│ │ FlowEditor.vue │ │
│ │ PropertyPanel │ │
│ │ ComponentsPanel │ │
│ │ Toolbar │ │
│ └─────────────────┘ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ 状态管理层 │ │
│ ├─────────────────┤ │
│ │ useStore │ │
│ │ useLogicFlow │ │
│ │ useCanvasSettings│ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
#### 2. 嵌入式组件模式切换
```
┌─────────────────────────────────────────────────────────┐
│ YysEditorEmbed 组件 │
├─────────────────────────────────────────────────────────┤
│ │
│ Props: { mode: 'preview' | 'edit', data: GraphData } │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 预览模式 │ │ 编辑模式 │ │
│ │ (preview) │ │ (edit) │ │
│ ├──────────────────┤ ├──────────────────┤ │
│ │ ✅ 画布(只读) │ │ ✅ 画布(可编辑) │ │
│ │ ❌ 工具栏 │ │ ✅ 工具栏 │ │
│ │ ❌ 组件库 │ │ ✅ 组件库 │ │
│ │ ❌ 属性面板 │ │ ✅ 属性面板 │ │
│ │ ❌ 交互 │ │ ✅ 完整交互 │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ Emits: { 'update:data', 'save', 'cancel', 'error' } │
│ │
└─────────────────────────────────────────────────────────┘
```
---
## 数据模型
### Props 接口
```typescript
interface YysEditorEmbedProps {
// 初始数据LogicFlow GraphData 格式)
data?: GraphData
// 模式preview预览/ edit编辑
mode?: 'preview' | 'edit'
// 尺寸
width?: string | number
height?: string | number
// UI 控制(仅在 edit 模式有效)
showToolbar?: boolean
showPropertyPanel?: boolean
showComponentPanel?: boolean
// 配置选项
config?: EditorConfig
}
// LogicFlow 标准数据格式
interface GraphData {
nodes: NodeData[]
edges: EdgeData[]
}
interface NodeData {
id: string
type: string
x: number
y: number
properties?: Record<string, any>
text?: { value: string }
}
interface EdgeData {
id: string
type: string
sourceNodeId: string
targetNodeId: string
properties?: Record<string, any>
}
// 编辑器配置
interface EditorConfig {
// 画布配置
grid?: boolean
snapline?: boolean
keyboard?: boolean
// 主题
theme?: 'light' | 'dark'
// 语言
locale?: 'zh' | 'ja' | 'en'
}
```
### Emits 接口
```typescript
interface YysEditorEmbedEmits {
// 数据变更(实时)
'update:data': (data: GraphData) => void
// 保存(用户点击保存按钮)
'save': (data: GraphData) => void
// 取消(用户点击取消按钮)
'cancel': () => void
// 错误
'error': (error: Error) => void
}
```
### 默认值
```typescript
const defaultProps = {
mode: 'edit',
width: '100%',
height: '600px',
showToolbar: true,
showPropertyPanel: true,
showComponentPanel: true,
config: {
grid: true,
snapline: true,
keyboard: true,
theme: 'light',
locale: 'zh'
}
}
```
---
## 实现细节
### 1. YysEditorEmbed.vue 组件结构
```vue
<template>
<div
class="yys-editor-embed"
:class="{ 'preview-mode': mode === 'preview' }"
:style="containerStyle"
>
<!-- 编辑模式完整 UI -->
<template v-if="mode === 'edit'">
<!-- 工具栏 -->
<Toolbar
v-if="showToolbar"
@save="handleSave"
@cancel="handleCancel"
/>
<!-- 主内容区 -->
<div class="editor-content">
<!-- 左侧组件库 -->
<ComponentsPanel v-if="showComponentPanel" />
<!-- 中间画布 -->
<FlowEditor
ref="flowEditorRef"
:initial-data="data"
@data-change="handleDataChange"
/>
<!-- 右侧属性面板 -->
<PropertyPanel v-if="showPropertyPanel" />
</div>
</template>
<!-- 预览模式只有画布 -->
<template v-else>
<FlowEditor
ref="flowEditorRef"
:initial-data="data"
:readonly="true"
/>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import FlowEditor from './components/flow/FlowEditor.vue'
import Toolbar from './components/Toolbar.vue'
import ComponentsPanel from './components/flow/ComponentsPanel.vue'
import PropertyPanel from './components/flow/PropertyPanel.vue'
import type { GraphData, EditorConfig } from './ts/schema'
// Props
const props = withDefaults(defineProps<{
data?: GraphData
mode?: 'preview' | 'edit'
width?: string | number
height?: string | number
showToolbar?: boolean
showPropertyPanel?: boolean
showComponentPanel?: boolean
config?: EditorConfig
}>(), {
mode: 'edit',
width: '100%',
height: '600px',
showToolbar: true,
showPropertyPanel: true,
showComponentPanel: true
})
// Emits
const emit = defineEmits<{
'update:data': [data: GraphData]
'save': [data: GraphData]
'cancel': []
'error': [error: Error]
}>()
// Refs
const flowEditorRef = ref<InstanceType<typeof FlowEditor>>()
// Computed
const containerStyle = computed(() => ({
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
height: typeof props.height === 'number' ? `${props.height}px` : props.height
}))
// Methods
const handleDataChange = (data: GraphData) => {
emit('update:data', data)
}
const handleSave = () => {
try {
const data = flowEditorRef.value?.getGraphData()
if (data) {
emit('save', data)
}
} catch (error) {
emit('error', error as Error)
}
}
const handleCancel = () => {
emit('cancel')
}
// 公开方法(供父组件调用)
const getGraphData = () => {
return flowEditorRef.value?.getGraphData()
}
const setGraphData = (data: GraphData) => {
flowEditorRef.value?.setGraphData(data)
}
defineExpose({
getGraphData,
setGraphData
})
// 监听 data 变化
watch(() => props.data, (newData) => {
if (newData && flowEditorRef.value) {
flowEditorRef.value.setGraphData(newData)
}
}, { deep: true })
// 初始化
onMounted(() => {
if (props.data && flowEditorRef.value) {
flowEditorRef.value.setGraphData(props.data)
}
})
</script>
<style scoped>
.yys-editor-embed {
display: flex;
flex-direction: column;
background: #f5f5f5;
overflow: hidden;
}
.editor-content {
display: flex;
flex: 1;
overflow: hidden;
}
.preview-mode {
background: transparent;
}
</style>
```
### 2. 状态隔离策略
**问题**:独立应用使用全局 Pinia store嵌入式组件需要隔离状态
**方案**:使用 provide/inject 创建局部状态
```typescript
// YysEditorEmbed.vue
import { provide } from 'vue'
import { createPinia } from 'pinia'
// 创建局部 Pinia 实例
const localPinia = createPinia()
provide('pinia', localPinia)
// 子组件使用局部 store
const store = useFilesStore(localPinia)
```
### 3. 样式隔离策略
**问题**:避免样式冲突
**方案**
1. 使用 scoped styles
2. 添加命名空间前缀 `.yys-editor-embed`
3. 使用 CSS Modules可选
```vue
<style scoped>
.yys-editor-embed {
/* 所有样式都在这个命名空间下 */
}
</style>
```
### 4. 构建配置
#### vite.config.ts库模式
```typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
build: {
lib: {
// 入口文件
entry: resolve(__dirname, 'src/YysEditorEmbed.vue'),
name: 'YysEditor',
// 输出文件名
fileName: (format) => `yys-editor.${format}.js`
},
rollupOptions: {
// 外部化依赖(不打包进库)
external: ['vue', 'element-plus'],
output: {
// 全局变量名
globals: {
vue: 'Vue',
'element-plus': 'ElementPlus'
}
}
}
}
})
```
#### package.json
```json
{
"name": "yys-editor",
"version": "1.0.0",
"type": "module",
"main": "./dist/yys-editor.umd.js",
"module": "./dist/yys-editor.es.js",
"types": "./dist/YysEditorEmbed.d.ts",
"exports": {
".": {
"import": "./dist/yys-editor.es.js",
"require": "./dist/yys-editor.umd.js",
"types": "./dist/YysEditorEmbed.d.ts"
},
"./style.css": "./dist/style.css"
},
"files": [
"dist"
],
"scripts": {
"dev": "vite",
"build": "vite build",
"build:lib": "vite build --config vite.config.lib.ts"
},
"peerDependencies": {
"vue": "^3.3.0",
"element-plus": "^2.9.0"
}
}
```
---
## 使用示例
### 在 onmyoji-wiki 中使用
```vue
<template>
<div class="yys-editor-block">
<!-- 预览模式 -->
<div v-if="!isEditing" @click="startEdit">
<YysEditorEmbed
mode="preview"
:data="flowData"
:height="400"
/>
<button class="edit-btn"> 编辑流程图</button>
</div>
<!-- 编辑模式弹窗 -->
<el-dialog v-model="isEditing" fullscreen>
<YysEditorEmbed
mode="edit"
:data="flowData"
:height="'100%'"
@save="handleSave"
@cancel="handleCancel"
@error="handleError"
/>
</el-dialog>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { YysEditorEmbed } from 'yys-editor'
import 'yys-editor/style.css'
const isEditing = ref(false)
const flowData = ref({
nodes: [],
edges: []
})
const startEdit = () => {
isEditing.value = true
}
const handleSave = (data) => {
flowData.value = data
isEditing.value = false
// 保存到文档
saveToDocument(data)
}
const handleCancel = () => {
isEditing.value = false
}
const handleError = (error) => {
console.error('编辑器错误:', error)
}
</script>
```
### 作为 npm 包安装
```bash
# 在 onmyoji-wiki 项目中
npm install file:../yys-editor
# 或发布到 npm 后
npm install yys-editor
```
---
## 测试计划
### 功能测试
#### 预览模式
- [ ] 正确渲染流程图
- [ ] 只读,无法编辑
- [ ] 不显示工具栏、组件库、属性面板
- [ ] 响应式尺寸
#### 编辑模式
- [ ] 完整编辑功能
- [ ] 工具栏正常工作
- [ ] 组件库可拖拽
- [ ] 属性面板可编辑
- [ ] 保存/取消按钮触发正确事件
#### 数据接口
- [ ] Props 传入数据正确渲染
- [ ] 数据变更触发 update:data 事件
- [ ] 保存触发 save 事件
- [ ] 取消触发 cancel 事件
- [ ] 错误触发 error 事件
#### 状态隔离
- [ ] 多个实例互不影响
- [ ] 不污染全局状态
- [ ] 样式不冲突
### 集成测试
#### 在 wiki 中集成
- [ ] 可以正常引入
- [ ] 预览模式正常显示
- [ ] 编辑模式正常工作
- [ ] 数据保存正确
- [ ] 样式不冲突
### 性能测试
- [ ] 打包体积合理(< 500KB gzipped
- [ ] 加载速度快
- [ ] 运行流畅,无卡顿
---
## 验收标准
### 功能完整性
- ✅ 支持预览和编辑模式
- ✅ 数据接口清晰Props + Emits
- ✅ 可以作为 npm 包引用
- ✅ 状态和样式隔离
### 兼容性
- ✅ 不影响独立应用功能
- ✅ 支持 Vue 3.3+
- ✅ 支持现代浏览器
### 文档完善
- ✅ API 文档
- ✅ 使用示例
- ✅ 集成指南
---
## 实现记录
### 2026-02-20
- 📝 创建组件化改造设计文档
- 📝 定义 Props 和 Emits 接口
- 📝 设计双模式架构
- 📝 规划状态隔离策略
---
**最后更新:** 2026-02-20
**文档版本:** v1.0.0