mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-03-05 15:05:27 +00:00
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:
231
src/TestEmbed.vue
Normal file
231
src/TestEmbed.vue
Normal 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
329
src/YysEditorEmbed.vue
Normal 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
11
src/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// 库入口文件
|
||||
import YysEditorEmbed from './YysEditorEmbed.vue'
|
||||
|
||||
// 导出组件
|
||||
export { YysEditorEmbed }
|
||||
|
||||
// 默认导出
|
||||
export default YysEditorEmbed
|
||||
|
||||
// 类型导出
|
||||
export * from './YysEditorEmbed.vue'
|
||||
Reference in New Issue
Block a user