From 1550f032d94141d12c5522cb8ab7e79aa1fe4c78 Mon Sep 17 00:00:00 2001 From: wanglin2 <1013335014@qq.com> Date: Thu, 28 Sep 2023 15:32:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=8F=E4=BD=9C=E5=A2=9E=E5=8A=A0=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/constants/defaultOptions.js | 7 +- simple-mind-map/src/core/render/node/Node.js | 10 ++ .../src/core/render/node/nodeCooperate.js | 104 ++++++++++++++++++ simple-mind-map/src/plugins/Cooperate.js | 79 ++++++++++++- web/src/pages/Edit/components/Edit.vue | 10 ++ 5 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 simple-mind-map/src/core/render/node/nodeCooperate.js diff --git a/simple-mind-map/src/constants/defaultOptions.js b/simple-mind-map/src/constants/defaultOptions.js index 6a4ad674..d7cc00fb 100644 --- a/simple-mind-map/src/constants/defaultOptions.js +++ b/simple-mind-map/src/constants/defaultOptions.js @@ -204,5 +204,10 @@ export const defaultOpt = { }, // 自定义标签的颜色 // {pass: 'green, unpass: 'red'} - tagsColorMap: {} + tagsColorMap: {}, + // 节点协作样式配置 + cooperateStyle: { + avatarSize: 22,// 头像大小 + fontSize: 12,// 如果是文字头像,那么文字的大小 + } } diff --git a/simple-mind-map/src/core/render/node/Node.js b/simple-mind-map/src/core/render/node/Node.js index d287755b..8dc840d1 100644 --- a/simple-mind-map/src/core/render/node/Node.js +++ b/simple-mind-map/src/core/render/node/Node.js @@ -6,6 +6,7 @@ import nodeExpandBtnMethods from './nodeExpandBtn' import nodeCommandWrapsMethods from './nodeCommandWraps' import nodeCreateContentsMethods from './nodeCreateContents' import nodeExpandBtnPlaceholderRectMethods from './nodeExpandBtnPlaceholderRect' +import nodeCooperateMethods from './nodeCooperate' import { CONSTANTS } from '../../../constants/constant' // 节点类 @@ -55,6 +56,8 @@ class Node { this.parent = opt.parent || null // 子节点 this.children = opt.children || [] + // 当前同时操作该节点的用户列表 + this.userList = [] // 节点内容的容器 this.group = null this.shapeNode = null // 节点形状节点 @@ -74,6 +77,7 @@ class Node { this._openExpandNode = null this._closeExpandNode = null this._fillExpandNode = null + this._userListGroup = null this._lines = [] this._generalizationLine = null this._generalizationNode = null @@ -121,6 +125,10 @@ class Node { Object.keys(nodeCreateContentsMethods).forEach(item => { this[item] = nodeCreateContentsMethods[item].bind(this) }) + // 协同相关 + Object.keys(nodeCooperateMethods).forEach((item) => { + this[item] = nodeCooperateMethods[item].bind(this) + }) // 初始化 this.getSize() } @@ -283,6 +291,7 @@ class Node { this.group.add(this.shapeNode) // 渲染一个隐藏的矩形区域,用来触发展开收起按钮的显示 this.renderExpandBtnPlaceholderRect() + this.createUserListNode() // 概要节点添加一个带所属节点id的类名 if (this.isGeneralization && this.generalizationBelongNode) { this.group.addClass('generalization_' + this.generalizationBelongNode.uid) @@ -527,6 +536,7 @@ class Node { } // 更新概要 this.renderGeneralization() + this.updateUserListNode() // 更新节点位置 let t = this.group.transform() // // 如果上次不在可视区内,且本次也不在,那么直接返回 diff --git a/simple-mind-map/src/core/render/node/nodeCooperate.js b/simple-mind-map/src/core/render/node/nodeCooperate.js new file mode 100644 index 00000000..970dfb2c --- /dev/null +++ b/simple-mind-map/src/core/render/node/nodeCooperate.js @@ -0,0 +1,104 @@ +import { Circle, G, Text, Image } from '@svgdotjs/svg.js' +import { generateColorByContent } from '../../../utils/index' + +// 协同相关功能 + +// 创建容器 +function createUserListNode() { + // 如果没有注册协作插件,那么需要创建 + if (!this.mindMap.cooperate) return + this._userListGroup = new G() + this.group.add(this._userListGroup) +} + +// 创建文本头像 +function createTextAvatar(item) { + const { avatarSize, fontSize } = this.mindMap.opt.cooperateStyle + const g = new G() + const str = item.isMore ? item.name : String(item.name)[0] + // 圆 + const circle = new Circle().size(avatarSize, avatarSize) + circle.fill({ + color: item.color || generateColorByContent(str) + }) + // 文本 + const text = new Text() + .text(str) + .fill({ + color: '#fff' + }) + .css({ + 'font-size': fontSize + }) + .dx(-fontSize / 2) + .dy((avatarSize - fontSize) / 2) + g.add(circle).add(text) + return g +} + +// 创建图片头像 +function createImageAvatar(item) { + const { avatarSize } = this.mindMap.opt.cooperateStyle + return new Image().load(item.avatar).size(avatarSize, avatarSize) +} + +// 更新渲染 +function updateUserListNode() { + if (!this._userListGroup) return + const { avatarSize } = this.mindMap.opt.cooperateStyle + this._userListGroup.clear() + // 根据当前节点长度计算最多能显示几个 + const length = this.userList.length + const maxShowCount = Math.floor(this.width / avatarSize) + const list = [] + if (length > maxShowCount) { + // 如果当前用户数量比最多能显示的多,最后需要显示一个提示信息 + list.push(...this.userList.slice(0, maxShowCount - 1), { + isMore: true, + name: '+' + (length - maxShowCount + 1) + }) + } else { + list.push(...this.userList) + } + list.forEach((item, index) => { + let node = null + if (item.avatar) { + node = this.createImageAvatar(item) + } else { + node = this.createTextAvatar(item) + } + node.x(index * avatarSize).cy(-avatarSize / 2) + this._userListGroup.add(node) + }) +} + +// 添加用户 +function addUser(userInfo) { + if ( + this.userList.find(item => { + return item.id == userInfo.id + }) + ) + return + this.userList.push(userInfo) + this.updateUserListNode() +} + +// 移除用户 +function removeUser(userInfo) { + const index = this.userList.findIndex(item => { + return item.id == userInfo.id + }) + if (index === -1) return + this.userList.splice(index, 1) + this.updateUserListNode() +} + +export default { + createUserListNode, + updateUserListNode, + createTextAvatar, + createImageAvatar, + addUser, + removeUser +} diff --git a/simple-mind-map/src/plugins/Cooperate.js b/simple-mind-map/src/plugins/Cooperate.js index 31fcf65f..877a4cdc 100644 --- a/simple-mind-map/src/plugins/Cooperate.js +++ b/simple-mind-map/src/plugins/Cooperate.js @@ -1,6 +1,6 @@ import * as Y from 'yjs' import { WebrtcProvider } from 'y-webrtc' -import { isSameObject, simpleDeepClone } from '../utils/index' +import { isSameObject, simpleDeepClone, getType, isUndef } from '../utils/index' // 协同插件 class Cooperate { @@ -12,7 +12,10 @@ class Cooperate { this.provider = new WebrtcProvider('demo-room', this.ydoc, { signaling: ['ws://10.16.83.11:4444'] }) + this.awareness = this.provider.awareness this.currentData = null + this.userInfo = null + this.currentAwarenessData = [] // 处理数据 if (this.mindMap.opt.data) { @@ -34,6 +37,14 @@ class Cooperate { // 监听思维导图改变 this.onDataChange = this.onDataChange.bind(this) this.mindMap.on('data_change', this.onDataChange) + + // 监听思维导图节点激活事件 + this.onNodeActive = this.onNodeActive.bind(this) + this.mindMap.on('node_active', this.onNodeActive) + + // 监听状态同步事件 + this.onAwareness = this.onAwareness.bind(this) + this.awareness.on('change', this.onAwareness) } // 解绑事件 @@ -66,6 +77,72 @@ class Cooperate { this.updateChanges(res) } + // 节点激活状态改变后触发状态显示同步 + onNodeActive(node, nodeList) { + if (this.userInfo) { + this.awareness.setLocalStateField(this.userInfo.name, { + userInfo: { + // 用户信息 + ...this.userInfo + }, + nodeIdList: nodeList.map(item => { + // 当前激活的节点id列表 + return item.uid + }) + }) + } + } + + // 设置用户信息 + /** + * { + * id: '', // 必传,用户唯一的id + * name: '', // 用户名称。name和avatar两个只传一个即可,如果都传了,会显示avatar + * avatar: '', // 用户头像 + * color: '' // 如果没有传头像,那么会以一个圆形来显示名称的第一个字,文字的颜色为白色,圆的颜色可以通过该字段设置 + * } + **/ + setUserInfo(userInfo) { + if ( + getType(userInfo) !== 'Object' || + isUndef(userInfo.id) || + (isUndef(userInfo.name) && isUndef(userInfo.avatar)) + ) + return + this.userInfo = userInfo || null + } + + // 监听状态同步事件 + onAwareness() { + const walk = (list, callback) => { + list.forEach(value => { + const userName = Object.keys(value)[0] + if (!userName) return + const data = value[userName] + const userInfo = data.userInfo + const nodeIdList = data.nodeIdList + nodeIdList.forEach(uid => { + const node = this.mindMap.renderer.findNodeByUid(uid) + if (node) { + callback(node, userInfo) + } + }) + }) + } + // 清除之前的状态 + walk(this.currentAwarenessData, (node, userInfo) => { + node.removeUser(userInfo) + }) + // 设置当前状态 + const data = Array.from(this.awareness.getStates().values()) + this.currentAwarenessData = data + walk(data, (node, userInfo) => { + // 不显示自己 + if (userInfo.id === this.userInfo.id) return + node.addUser(userInfo) + }) + } + // 将树结构转平级对象 /* { diff --git a/web/src/pages/Edit/components/Edit.vue b/web/src/pages/Edit/components/Edit.vue index e0a1a609..480b701c 100644 --- a/web/src/pages/Edit/components/Edit.vue +++ b/web/src/pages/Edit/components/Edit.vue @@ -389,6 +389,16 @@ export default { if (hasFileURL) { this.$bus.$emit('handle_file_url') } + if (this.$route.query.userName) { + this.mindMap.cooperate.setUserInfo({ + id: Math.random(), + name: this.$route.query.userName, + color: ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399'][ + Math.floor(Math.random() * 5) + ], + avatar: Math.random() > 0.5 ? 'https://img0.baidu.com/it/u=4270674549,2416627993&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1696006800&t=4d32871d14a7224a4591d0c3c7a97311' : '' + }) + } }, // url中是否存在要打开的文件