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

16 KiB
Raw Blame History

组件化改造设计文档

背景与目标

为什么需要组件化改造

当前状态

  • 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 接口

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 接口

interface YysEditorEmbedEmits {
  // 数据变更(实时)
  'update:data': (data: GraphData) => void

  // 保存(用户点击保存按钮)
  'save': (data: GraphData) => void

  // 取消(用户点击取消按钮)
  'cancel': () => void

  // 错误
  'error': (error: Error) => void
}

默认值

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 组件结构

<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 创建局部状态

// 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可选
<style scoped>
.yys-editor-embed {
  /* 所有样式都在这个命名空间下 */
}
</style>

4. 构建配置

vite.config.ts库模式

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

{
  "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 中使用

<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 包安装

# 在 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