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
This commit is contained in:
2026-02-20 17:23:59 +08:00
parent 92557d553b
commit 15bae3be81
13 changed files with 3553 additions and 424 deletions

231
src/TestEmbed.vue Normal file
View File

@@ -0,0 +1,231 @@
<template>
<div class="test-page">
<h1>YysEditorEmbed 测试页面</h1>
<div class="test-section">
<h2>测试 1编辑模式</h2>
<div class="controls">
<button @click="testSave">测试保存</button>
<button @click="testGetData">获取数据</button>
<button @click="testSetData">设置数据</button>
</div>
<YysEditorEmbed
ref="editorRef"
mode="edit"
:data="testData"
:height="500"
@save="handleSave"
@cancel="handleCancel"
@error="handleError"
/>
</div>
<div class="test-section">
<h2>测试 2预览模式</h2>
<button @click="togglePreviewMode">切换到编辑模式</button>
<YysEditorEmbed
mode="preview"
:data="testData"
:height="400"
/>
</div>
<div class="test-section">
<h2>测试 3自定义配置</h2>
<div class="controls">
<label>
<input type="checkbox" v-model="showToolbar" />
显示工具栏
</label>
<label>
<input type="checkbox" v-model="showComponentPanel" />
显示组件库
</label>
<label>
<input type="checkbox" v-model="showPropertyPanel" />
显示属性面板
</label>
</div>
<YysEditorEmbed
mode="edit"
:data="testData"
:height="500"
:show-toolbar="showToolbar"
:show-component-panel="showComponentPanel"
:show-property-panel="showPropertyPanel"
/>
</div>
<div class="test-section">
<h2>数据输出</h2>
<pre>{{ JSON.stringify(outputData, null, 2) }}</pre>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import YysEditorEmbed from '../src/YysEditorEmbed.vue'
const editorRef = ref()
const showToolbar = ref(true)
const showComponentPanel = ref(true)
const showPropertyPanel = ref(true)
const outputData = ref(null)
const testData = ref({
nodes: [
{
id: 'node1',
type: 'rect',
x: 100,
y: 100,
text: { value: '测试节点 1' },
properties: {
width: 120,
height: 60
}
},
{
id: 'node2',
type: 'rect',
x: 300,
y: 100,
text: { value: '测试节点 2' },
properties: {
width: 120,
height: 60
}
}
],
edges: [
{
id: 'edge1',
type: 'polyline',
sourceNodeId: 'node1',
targetNodeId: 'node2'
}
]
})
const handleSave = (data: any) => {
console.log('保存数据:', data)
outputData.value = data
alert('数据已保存!查看控制台和下方输出。')
}
const handleCancel = () => {
console.log('取消编辑')
alert('已取消编辑')
}
const handleError = (error: Error) => {
console.error('错误:', error)
alert(`发生错误: ${error.message}`)
}
const testSave = () => {
const data = editorRef.value?.getGraphData()
console.log('手动获取数据:', data)
outputData.value = data
}
const testGetData = () => {
const data = editorRef.value?.getGraphData()
console.log('获取数据:', data)
alert('数据已输出到控制台')
}
const testSetData = () => {
const newData = {
nodes: [
{
id: 'node3',
type: 'circle',
x: 200,
y: 200,
text: { value: '新节点' },
properties: {
r: 40
}
}
],
edges: []
}
editorRef.value?.setGraphData(newData)
alert('已设置新数据')
}
const togglePreviewMode = () => {
alert('切换模式功能需要在实际应用中实现(通过改变 mode prop')
}
</script>
<style scoped>
.test-page {
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
h1 {
margin-bottom: 30px;
color: #333;
}
.test-section {
margin-bottom: 40px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.test-section h2 {
margin-bottom: 15px;
color: #409eff;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
button {
padding: 8px 16px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #66b1ff;
}
label {
display: flex;
align-items: center;
gap: 5px;
padding: 8px 12px;
background: #f5f5f5;
border-radius: 4px;
cursor: pointer;
}
label input[type="checkbox"] {
cursor: pointer;
}
pre {
background: #f6f8fa;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-size: 12px;
line-height: 1.5;
}
</style>

329
src/YysEditorEmbed.vue Normal file
View File

@@ -0,0 +1,329 @@
<template>
<div
class="yys-editor-embed"
:class="{ 'preview-mode': mode === 'preview', 'edit-mode': mode === 'edit' }"
:style="containerStyle"
>
<!-- 编辑模式完整 UI -->
<template v-if="mode === 'edit'">
<!-- 工具栏 -->
<Toolbar
v-if="showToolbar"
:is-embed="true"
@save="handleSave"
@cancel="handleCancel"
/>
<!-- 主内容区 -->
<div class="editor-content" :style="{ height: contentHeight }">
<!-- 左侧组件库 -->
<ComponentsPanel v-if="showComponentPanel" />
<!-- 中间画布 + 右侧属性面板 -->
<FlowEditor
ref="flowEditorRef"
:height="contentHeight"
/>
</div>
</template>
<!-- 预览模式只有画布只读 -->
<template v-else>
<div class="preview-container" :style="{ height: containerHeight }">
<div class="container" ref="previewContainerRef"></div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onBeforeUnmount, provide } from 'vue'
import { createPinia } from 'pinia'
import LogicFlow from '@logicflow/core'
import '@logicflow/core/lib/style/index.css'
import { Snapshot, MiniMap, Control } from '@logicflow/extension'
import '@logicflow/extension/lib/style/index.css'
import FlowEditor from './components/flow/FlowEditor.vue'
import Toolbar from './components/Toolbar.vue'
import ComponentsPanel from './components/flow/ComponentsPanel.vue'
import { useFilesStore } from '@/ts/useStore'
import { setLogicFlowInstance, destroyLogicFlowInstance, getLogicFlowInstance } from '@/ts/useLogicFlow'
import { register } from '@logicflow/vue-node-registry'
import ImageNode from './components/flow/nodes/common/ImageNode.vue'
import AssetSelectorNode from './components/flow/nodes/common/AssetSelectorNode.vue'
import TextNode from './components/flow/nodes/common/TextNode.vue'
import TextNodeModel from './components/flow/nodes/common/TextNodeModel'
import VectorNode from './components/flow/nodes/common/VectorNode.vue'
import VectorNodeModel from './components/flow/nodes/common/VectorNodeModel'
// 类型定义
export interface GraphData {
nodes: NodeData[]
edges: EdgeData[]
}
export interface NodeData {
id: string
type: string
x: number
y: number
properties?: Record<string, any>
text?: { value: string }
}
export interface EdgeData {
id: string
type: string
sourceNodeId: string
targetNodeId: string
properties?: Record<string, any>
}
export interface EditorConfig {
grid?: boolean
snapline?: boolean
keyboard?: boolean
theme?: 'light' | 'dark'
locale?: 'zh' | 'ja' | 'en'
}
// 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,
config: () => ({
grid: true,
snapline: true,
keyboard: true,
theme: 'light',
locale: 'zh'
})
})
// Emits
const emit = defineEmits<{
'update:data': [data: GraphData]
'save': [data: GraphData]
'cancel': []
'error': [error: Error]
}>()
// 创建局部 Pinia 实例(状态隔离)
const localPinia = createPinia()
provide('pinia', localPinia)
// Refs
const flowEditorRef = ref<InstanceType<typeof FlowEditor>>()
const previewContainerRef = ref<HTMLElement | null>(null)
const previewLf = ref<LogicFlow | null>(null)
// Computed
const containerStyle = computed(() => ({
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
height: typeof props.height === 'number' ? `${props.height}px` : props.height
}))
const containerHeight = computed(() => {
return typeof props.height === 'number' ? `${props.height}px` : props.height
})
const contentHeight = computed(() => {
if (props.showToolbar) {
const toolbarHeight = 48
const totalHeight = typeof props.height === 'number' ? props.height : 600
return `${totalHeight - toolbarHeight}px`
}
return containerHeight.value
})
// 初始化预览模式的 LogicFlow
const initPreviewMode = () => {
if (!previewContainerRef.value) return
// 注册自定义节点
register({
type: 'imageNode',
component: ImageNode
})
register({
type: 'assetSelector',
component: AssetSelectorNode
})
register({
type: 'textNode',
component: TextNode,
model: TextNodeModel
})
register({
type: 'vectorNode',
component: VectorNode,
model: VectorNodeModel
})
// 创建 LogicFlow 实例(只读模式)
previewLf.value = new LogicFlow({
container: previewContainerRef.value,
width: previewContainerRef.value.offsetWidth,
height: previewContainerRef.value.offsetHeight,
grid: false,
keyboard: {
enabled: false
},
// 禁用所有交互
isSilentMode: true,
stopScrollGraph: true,
stopZoomGraph: true,
stopMoveGraph: true,
adjustNodePosition: false,
plugins: [Snapshot, MiniMap, Control]
})
// 渲染数据
if (props.data) {
previewLf.value.render(props.data)
}
}
// Methods
const handleSave = () => {
try {
const data = getGraphData()
if (data) {
emit('save', data)
}
} catch (error) {
emit('error', error as Error)
}
}
const handleCancel = () => {
emit('cancel')
}
// 公开方法(供父组件调用)
const getGraphData = (): GraphData | null => {
if (props.mode === 'edit') {
const lfInstance = getLogicFlowInstance()
if (lfInstance) {
return lfInstance.getGraphRawData() as GraphData
}
} else if (props.mode === 'preview' && previewLf.value) {
return previewLf.value.getGraphRawData() as GraphData
}
return null
}
const setGraphData = (data: GraphData) => {
if (props.mode === 'edit') {
const lfInstance = getLogicFlowInstance()
if (lfInstance) {
lfInstance.render(data)
}
} else if (props.mode === 'preview' && previewLf.value) {
previewLf.value.render(data)
}
}
defineExpose({
getGraphData,
setGraphData
})
// 监听 data 变化
watch(() => props.data, (newData) => {
if (newData) {
setGraphData(newData)
}
}, { deep: true })
// 监听模式变化
watch(() => props.mode, (newMode) => {
if (newMode === 'preview') {
// 切换到预览模式,初始化预览 LogicFlow
setTimeout(() => {
initPreviewMode()
}, 100)
}
})
// 初始化
onMounted(() => {
if (props.mode === 'preview') {
initPreviewMode()
} else if (props.mode === 'edit') {
// 编辑模式由 FlowEditor 组件初始化
// 等待 FlowEditor 初始化完成后加载数据
setTimeout(() => {
if (props.data) {
setGraphData(props.data)
}
}, 500)
}
})
// 清理
onBeforeUnmount(() => {
if (previewLf.value) {
previewLf.value.destroy()
previewLf.value = null
}
destroyLogicFlowInstance()
})
</script>
<style scoped>
.yys-editor-embed {
display: flex;
flex-direction: column;
background: #f5f5f5;
overflow: hidden;
position: relative;
}
.editor-content {
display: flex;
flex: 1;
overflow: hidden;
}
.preview-mode {
background: transparent;
}
.preview-container {
width: 100%;
height: 100%;
position: relative;
}
.preview-container .container {
width: 100%;
height: 100%;
}
/* 预览模式下隐藏所有控制元素 */
.preview-mode :deep(.lf-control),
.preview-mode :deep(.lf-mini-map),
.preview-mode :deep(.lf-menu) {
display: none !important;
}
/* 预览模式下禁用鼠标交互 */
.preview-mode :deep(.lf-canvas-overlay) {
pointer-events: none;
}
</style>

11
src/index.js Normal file
View File

@@ -0,0 +1,11 @@
// 库入口文件
import YysEditorEmbed from './YysEditorEmbed.vue'
// 导出组件
export { YysEditorEmbed }
// 默认导出
export default YysEditorEmbed
// 类型导出
export * from './YysEditorEmbed.vue'