Skip to content

Commit e11b4d0

Browse files
committed
feat: methods sync
1 parent 5359f22 commit e11b4d0

9 files changed

Lines changed: 167 additions & 31 deletions

File tree

packages/canvas/container/src/CanvasContainer.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ export default {
475475
moveUpSharedNode,
476476
updateStyleNode,
477477
updatePropsNode,
478+
updateMethodNode,
478479
remoteStates
479480
} = useCollabSchema({
480481
roomId: 'schema-yjs',
@@ -489,6 +490,7 @@ export default {
489490
moveUpSharedNode,
490491
updateStyleNode,
491492
updatePropsNode,
493+
updateMethodNode,
492494
remoteStates
493495
})
494496

packages/multi-person-collaboration/src/composables/useCollabSchema.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
22
import { onUnmounted, toRaw } from 'vue'
33
import { SchemaManager } from '../services/schemaManager'
4-
import { POSITION, type Node, type PositionType, type RootNode, type UserAwareness } from '../type'
4+
import {
5+
POSITION,
6+
type Node,
7+
type PositionType,
8+
type RootNode,
9+
type UpdateMethodsOperation,
10+
type UserAwareness
11+
} from '../type'
512
import { useYjs } from './useYjs'
613
import { PORT } from '../config'
714
import { useAwareness } from './useAwareness'
@@ -79,10 +86,15 @@ export function useCollabSchema(options: UseCollabSchemaOptions) {
7986
}
8087

8188
// settings, 修改节点属性
82-
const updatePropsNode = (newProps: Record<any, any>, nodeId: string, overwrite: boolean) => {
89+
const updatePropsNode = (newProps: Record<string, any>, nodeId: string, overwrite: boolean) => {
8390
schemaModel.updatedNodeProps(newProps, nodeId, overwrite)
8491
}
8592

93+
// 根节点保存 methods
94+
const updateMethodNode = (operation: UpdateMethodsOperation) => {
95+
schemaModel.updatedNodeMethods(operation)
96+
}
97+
8698
// 用户信息同步 方法
8799
const updateUserSelection = (selectedNode: any) => {
88100
updateLocalStateField('selection', selectedNode)
@@ -117,6 +129,7 @@ export function useCollabSchema(options: UseCollabSchemaOptions) {
117129
moveUpSharedNode,
118130
moveDownSharedNode,
119131
updateStyleNode,
120-
updatePropsNode
132+
updatePropsNode,
133+
updateMethodNode
121134
}
122135
}

packages/multi-person-collaboration/src/models/NodeSchemaModel.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import * as Y from 'yjs'
22
import { useCanvas } from '@opentiny/tiny-engine-meta-register'
3-
import { type NodeSchema, type Node, type PositionType, POSITION, type InsertOptions, type PageSchema } from '../type'
3+
import {
4+
type NodeSchema,
5+
type Node,
6+
type PositionType,
7+
POSITION,
8+
type InsertOptions,
9+
type PageSchema,
10+
type UpdateMethodsOperation
11+
} from '../type'
412
import { OperationHandler } from '../operation/operationHandler '
513

614
/**
@@ -78,6 +86,11 @@ export class NodeSchemaModel {
7886
this.operationHandler.updatedProps({ newProps, nodeId, overwrite })
7987
}
8088

89+
// methods, 添加或者更新事件
90+
public updatedNodeMethods(operation: UpdateMethodsOperation) {
91+
this.operationHandler.updatedMethods(operation)
92+
}
93+
8194
// insert 操作
8295
private insert(parentId: string, newNodeData: Node, position: string, referTargetNodeId?: string) {
8396
this.operationHandler.insert({

packages/multi-person-collaboration/src/operation/operationHandler .ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
Node,
55
NodeOperation,
66
PageSchema,
7+
UpdateMethodsOperation,
78
UpdatePropsOperation,
89
UpdateStyleOperation
910
} from '../type'
@@ -214,6 +215,26 @@ export class OperationHandler {
214215
node.set('props', yNewProps)
215216
}
216217

218+
// 添加 或 更新 methods
219+
public updatedMethods(operation: UpdateMethodsOperation) {
220+
const methods = operation.methods
221+
222+
if (operation.type === 'root') {
223+
// 根节点直接设置 methods 不需要 id
224+
this.yMap.set('methods', methods)
225+
} else if (operation.type === 'node') {
226+
const { nodeId, methodsName, methods } = operation
227+
const node = this.getYNode(nodeId)
228+
if (node) {
229+
const nodeProps = node.get('props')
230+
nodeProps.set(methodsName, {
231+
methods,
232+
meta: { nodeId }
233+
})
234+
}
235+
}
236+
}
237+
217238
// 重建整个映射(刷新后可以手动调用)
218239
public rebuildYNodeMap(rootSchema: PageSchema) {
219240
this.yNodeMap.clear()

packages/multi-person-collaboration/src/services/schemaManager.ts

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as Y from 'yjs'
33
import { NodeSchemaModel } from '../models/NodeSchemaModel'
44
import { DocManager } from './docManager'
55
import type { RootNode } from '../type'
6-
import { fromYjs, toYjs } from '../utils'
6+
import { fromYjs, sanitizeSchema, toYjs } from '../utils'
77
import { toRaw } from 'vue'
88
import type { YjsProvider } from './providerManager'
99
import { IGNORE_OBSERVER_ORIGIN, ROOT_SCHEMA_MAP } from '../config'
@@ -25,6 +25,14 @@ type DiffPatch =
2525
| { type: 'style-update'; path: (string | number)[]; css: string }
2626
| { type: 'class-add'; path: (string | number)[]; nodeId: string; className: string }
2727
| { type: 'props-update'; path: (string | number)[]; props: Record<any, any>; meta: Record<any, any> }
28+
| { type: 'methods-add-root'; path: (string | number)[]; methods: Record<string, any> }
29+
| {
30+
type: 'methods-add-node'
31+
path: (string | number)[]
32+
methods: Record<string, any>
33+
methodsName: string
34+
nodeId: string
35+
}
2836

2937
/**
3038
* SchemaManager 类,负责管理 Yjs 中的 NodeSchema 文档
@@ -90,8 +98,14 @@ export class SchemaManager {
9098
console.log(`[${docName}] Initial sync complete. Applying full schema to UI.`)
9199

92100
// 安全时间点,用 Yjs 的权威数据完全覆盖 UI
93-
const remoteSchema = fromYjs(yMap)
94-
useCanvas().importSchema(remoteSchema)
101+
const rawRemoteSchema = fromYjs(yMap)
102+
103+
// 净化 schema
104+
const INTERNAL_YJS_KEYS = ['meta'] // 定义需要过滤的键
105+
const cleanSchema = sanitizeSchema(rawRemoteSchema, INTERNAL_YJS_KEYS)
106+
107+
// 使用干净的 schema 来覆盖 UI
108+
useCanvas().importSchema(cleanSchema)
95109

96110
// 标记初始同步已完成
97111
this.initialSyncDone.set(docName, true)
@@ -102,7 +116,7 @@ export class SchemaManager {
102116
// 冷启动:第一次启动,远端无数据 -> 用本地初始化
103117
if (yMap.size === 0) {
104118
ydoc.transact(() => {
105-
toYjs(yMap!, pageSchema)
119+
toYjs(yMap!, toRaw(useCanvas().getPageSchema()))
106120
}, IGNORE_OBSERVER_ORIGIN)
107121
}
108122
} else {
@@ -191,7 +205,6 @@ export class SchemaManager {
191205
| { yRoot: Y.Map<any>; cb: (events: Y.YEvent<any>[], tr: Y.Transaction) => void }
192206
| undefined
193207

194-
// 判断时也使用正确的大小写
195208
if (prev?.yRoot && prev?.cb) {
196209
try {
197210
prev.yRoot.unobserveDeep(prev.cb)
@@ -239,6 +252,7 @@ export class SchemaManager {
239252
// Map 变更
240253
if (event.target instanceof Y.Map) {
241254
const yMapNode = event.target
255+
const regex = /^on[A-Z][A-Za-z]*$/
242256

243257
event.changes.keys.forEach((change, key) => {
244258
// 软删除
@@ -283,16 +297,37 @@ export class SchemaManager {
283297
// Props 属性更新同步逻辑
284298
if (change.action === 'add' || change.action === 'update') {
285299
const newProps = yMapNode.get('props').toJSON()
286-
const meta = newProps.meta // meta 包含操作的 nodeId 和 overwrite
287-
288-
delete newProps.meta // 删除元数据,保持 props 不被污染
300+
const { meta, ...cleanProps } = newProps
289301
patches.push({
290302
type: 'props-update',
291303
path: event.path,
292-
props: newProps,
304+
props: cleanProps,
293305
meta
294306
})
295307
}
308+
} else if (key === 'methods') {
309+
// 根节点添加 事件函数
310+
if (change.action === 'add' || change.action === 'update') {
311+
const newMethods = yMapNode.get('methods')
312+
patches.push({
313+
type: 'methods-add-root',
314+
path: event.path,
315+
methods: newMethods
316+
})
317+
}
318+
} else if (regex.test(key)) {
319+
// 先判断非根节点 添加时间函数
320+
if (change.action === 'add' || change.action === 'update') {
321+
const newObj = yMapNode.get(key)
322+
const { meta, methods } = newObj
323+
patches.push({
324+
type: 'methods-add-node',
325+
path: event.path,
326+
methods,
327+
methodsName: key,
328+
nodeId: meta.nodeId
329+
})
330+
}
296331
}
297332
})
298333
}
@@ -413,10 +448,9 @@ export class SchemaManager {
413448
break
414449
}
415450
case 'style-update': {
416-
const { updateSchema } = useCanvas()
417451
const strStyle = patch.css
418452

419-
updateSchema({ css: strStyle })
453+
useCanvas().updateSchema({ css: strStyle })
420454
break
421455
}
422456
case 'props-update': {
@@ -431,6 +465,19 @@ export class SchemaManager {
431465
})
432466
break
433467
}
468+
case 'methods-add-root': {
469+
const { methods } = patch
470+
useCanvas().updateSchema({ methods })
471+
break
472+
}
473+
case 'methods-add-node': {
474+
const { methods, methodsName, nodeId } = patch
475+
const targetNode = useCanvas().getNode(nodeId, false)
476+
477+
targetNode[methodsName] = methods
478+
useMessage().publish({ topic: 'schemaChange', data: {} })
479+
break
480+
}
434481
default:
435482
break
436483
}

packages/multi-person-collaboration/src/type.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,15 @@ export interface UpdateStyleOperation {
9797
}
9898

9999
export interface UpdatePropsOperation {
100-
newProps: Record<string, string>
100+
newProps: Record<string, any>
101101
nodeId: string
102102
overwrite: boolean
103103
}
104104

105+
export type UpdateMethodsOperation =
106+
| { type: 'root'; methods: Record<string, any> }
107+
| { type: 'node'; nodeId: string; methodsName: string; methods: Record<string, any>; params: any }
108+
105109
export interface UserAwareness {
106110
id?: string | number
107111
name: string

packages/multi-person-collaboration/src/utils/index.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,37 @@ export const getValueByPath = (obj: any, path: (string | number)[]): any => {
8484
return acc[key]
8585
}, obj)
8686
}
87+
88+
/**
89+
* 递归地净化一个从 Yjs 转换而来的 schema 对象。
90+
* 它会移除所有内部使用的键(如事件总线、元数据等)。
91+
*
92+
* @param schema - 从 fromYjs() 得到的原始 schema 对象。
93+
* @param keysToFilter - 一个包含需要被移除的键名的数组。
94+
* @returns 一个只包含纯 UI 数据的、干净的 schema 对象。
95+
*/
96+
export function sanitizeSchema(schema: any, keysToFilter: string[]): any {
97+
// 如果输入不是对象或为 null,直接返回
98+
if (typeof schema !== 'object' || schema === null) {
99+
return schema
100+
}
101+
102+
// 如果是数组,则递归地净化数组中的每一个元素
103+
if (Array.isArray(schema)) {
104+
return schema.map((item) => sanitizeSchema(item, keysToFilter))
105+
}
106+
107+
// 如果是对象,则创建一个新对象,并过滤掉不需要的键
108+
const sanitizedObject: { [key: string]: any } = {}
109+
for (const key in schema) {
110+
// 检查当前键是否在过滤列表中
111+
if (keysToFilter.includes(key)) {
112+
continue // 跳过这个内部键
113+
}
114+
115+
// 递归地净化子属性
116+
sanitizedObject[key] = sanitizeSchema(schema[key], keysToFilter)
117+
}
118+
119+
return sanitizedObject
120+
}

packages/plugins/script/src/js/method.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { string2Ast, ast2String, insertName, formatString } from '@opentiny/tiny
1717
import { constants } from '@opentiny/tiny-engine-utils'
1818
import { lint } from '@opentiny/tiny-engine-common/js/linter'
1919
import { isFunction } from '@opentiny/vue-renderless/grid/static'
20-
import { useCollabMonaco } from '@opentiny/tiny-engine-multi-person-collaboration'
20+
import { useRealtimeCollab } from '@opentiny/tiny-engine-meta-register'
2121

2222
const { SCHEMA_DATA_TYPE } = constants
2323

@@ -90,6 +90,12 @@ export const saveMethod = ({ name, content }) => {
9090
}
9191

9292
useCanvas().updateSchema({ methods: { ...methods, [name]: methodItem } })
93+
94+
// 多人协作,同步 methods
95+
useRealtimeCollab().updateMethodNode({
96+
type: 'root',
97+
methods: { ...methods, [name]: methodItem }
98+
})
9399
}
94100

95101
const saveMethods = async () => {
@@ -231,19 +237,6 @@ export default ({ emit }) => {
231237
state.script = getScriptString()
232238
monaco.value?.focus()
233239
window.dispatchEvent(new Event('resize'))
234-
const currentUser = {
235-
id: 2,
236-
name: 'Bob',
237-
color: '#ff0000ff',
238-
avatarUrl: 'https://i.pravatar.cc/150?img=2'
239-
}
240-
// 代码的协同编辑
241-
useCollabMonaco({
242-
currentUser,
243-
editorRef: monaco,
244-
roomId: 'page-js-Yjs',
245-
fieldName: 'page-js'
246-
})
247240
})
248241
})
249242

packages/settings/events/src/components/BindEventsDialog.vue

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ import {
4343
META_APP
4444
} from '@opentiny/tiny-engine-meta-register'
4545
import { Button, DialogBox, TinyAlert } from '@opentiny/vue'
46-
import { nextTick, provide, reactive, ref } from 'vue'
46+
import { nextTick, provide, reactive, ref, toRaw } from 'vue'
4747
import MagicString from 'magic-string'
4848
import meta from '../../meta'
49+
import { useRealtimeCollab } from '@opentiny/tiny-engine-meta-register'
4950
5051
const dialogVisible = ref(false)
5152
@@ -125,6 +126,14 @@ export default {
125126
nodeProps[eventName].value = `this.${name}`
126127
127128
useHistory().addHistory()
129+
130+
// 多人协作, 同步methods
131+
useRealtimeCollab().updateMethodNode({
132+
type: 'node',
133+
nodeId: pageState?.currentSchema?.id,
134+
methodsName: eventName,
135+
methods: toRaw(nodeProps[eventName])
136+
})
128137
}
129138
130139
const resetTipError = () => {

0 commit comments

Comments
 (0)