Skip to content

Commit 22d0ad5

Browse files
committed
Merge branch 'main' of github.com:byteGanYue/DocCollab
2 parents bbcddc1 + 1b71a1a commit 22d0ad5

1 file changed

Lines changed: 95 additions & 233 deletions

File tree

packages/doc-docs/关于训练营/邓雄峰答辩.md

Lines changed: 95 additions & 233 deletions
Original file line numberDiff line numberDiff line change
@@ -75,263 +75,125 @@
7575

7676
- **评论定位**:使用Yjs相对位置系统(Y.createRelativePositionFromTypeIndex)解决文档编辑导致的**评论漂移**问题
7777

78-
```javascript
79-
// 添加评论方法
80-
const addComment = useCallback((startIndex, endIndex, content, author) => {
81-
if (!yCommentsRef.current || !yTextRef.current || !editorRef.current) {
82-
console.error('Comments, text reference or editor not initialized');
83-
return;
84-
}
85-
86-
try {
87-
const editor = editorRef.current;
88-
89-
// 保存当前选区
90-
const savedSelection = editor.selection;
91-
92-
// 使用全局索引找到对应的 Slate 范围
93-
let count = 0;
94-
let anchor = null;
95-
let focus = null;
96-
97-
for (const [node, path] of Node.texts(editor)) {
98-
const len = Node.string(node).length;
99-
if (anchor === null && count + len >= startIndex) {
100-
anchor = { path, offset: startIndex - count };
101-
}
102-
if (focus === null && count + len >= endIndex) {
103-
focus = { path, offset: endIndex - count };
104-
break;
105-
}
106-
count += len;
107-
}
108-
109-
if (!anchor || !focus) {
110-
console.error('Failed to find text range for comment');
111-
return;
112-
}
113-
114-
// 设置选区到要评论的文本
115-
const commentRange = { anchor, focus };
116-
Transforms.select(editor, commentRange);
117-
118-
// 生成唯一的评论ID
119-
const commentId = Date.now().toString();
120-
121-
// 直接添加评论标记到选中的文本
122-
Editor.addMark(editor, 'comment', {
123-
id: commentId,
124-
content,
125-
author,
126-
time: Date.now(),
127-
});
128-
129-
// 恢复原始选区
130-
if (savedSelection) {
131-
Transforms.select(editor, savedSelection);
132-
}
133-
134-
// 清除编辑器的活动标记状态,防止影响后续输入
135-
// 注意:这里只清除活动标记,不影响已经应用到文本节点上的标记
136-
editor.marks = null;
137-
138-
// 延迟清除活动标记,确保不影响后续输入
139-
setTimeout(() => {
140-
editor.marks = null;
141-
}, 0);
142-
143-
console.log('Comment added successfully');
144-
145-
// 同时也保存到 Yjs 数组中以便协同
146-
if (startIndex !== undefined && endIndex !== undefined) {
147-
// 确保索引在有效范围内
148-
const validStartIndex = Math.max(
149-
0,
150-
Math.min(startIndex, yTextRef.current.length),
151-
);
152-
const validEndIndex = Math.max(
153-
validStartIndex,
154-
Math.min(endIndex, yTextRef.current.length),
155-
);
156-
157-
// 创建相对位置
158-
const start = Y.createRelativePositionFromTypeIndex(
159-
yTextRef.current,
160-
validStartIndex,
161-
);
162-
const end = Y.createRelativePositionFromTypeIndex(
163-
yTextRef.current,
164-
validEndIndex,
165-
);
166-
167-
if (start && end) {
168-
const startJSON = JSON.stringify(start);
169-
const endJSON = JSON.stringify(end);
170-
171-
// 添加评论到 Yjs 数组
172-
yCommentsRef.current.push([
173-
{
174-
id: commentId,
175-
start: startJSON,
176-
end: endJSON,
177-
content,
178-
author,
179-
time: Date.now(),
180-
},
181-
]);
182-
}
183-
}
184-
} catch (error) {
185-
console.error('Error adding comment:', error);
186-
}
187-
}, []);
188-
```
78+
下面是协同评论功能核心的代码实现:
18979

19080
```javascript
191-
// 删除评论方法
192-
const removeComment = useCallback(
193-
commentId => {
194-
try {
195-
if (!editor || !commentId) {
196-
console.error('Editor or commentId is missing');
197-
return;
198-
}
81+
// 1. 评论数据结构定义 (存储在Yjs中)
82+
const commentStructure = {
83+
id: "unique_id", // 唯一标识
84+
start: "relative_pos", // 起始位置(JSON序列化的Y.RelativePosition)
85+
end: "relative_pos", // 结束位置
86+
content: "评论内容",
87+
author: "作者",
88+
time: 1672531200000 // 时间戳
89+
};
90+
91+
// 2. 核心协同逻辑 (useCollaborativeEditor.jsx)
92+
// 初始化Yjs数据结构
93+
useEffect(() => {
94+
if (!docRef.current) return;
95+
yCommentsRef.current = docRef.current.getArray('comments'); // Yjs共享数组
96+
yTextRef.current = docRef.current.get('content', Y.XmlText); // 共享文本
97+
}, [docRef]);
98+
99+
// 3. 添加评论的核心方法
100+
const addComment = useCallback((startIndex, endIndex, content, author) => {
101+
// 生成相对位置(解决编辑漂移问题)
102+
const startPos = Y.createRelativePositionFromTypeIndex(
103+
yTextRef.current,
104+
startIndex
105+
);
106+
const endPos = Y.createRelativePositionFromTypeIndex(
107+
yTextRef.current,
108+
endIndex
109+
);
199110

200-
console.log('Removing comment:', commentId);
111+
// 存储到Yjs共享数组
112+
yCommentsRef.current.push([{
113+
id: Date.now().toString(),
114+
start: JSON.stringify(startPos),
115+
end: JSON.stringify(endPos),
116+
content,
117+
author,
118+
time: Date.now()
119+
}]);
120+
121+
// 添加Slate标记(本地渲染)
122+
Editor.addMark(editor, 'comment', {
123+
id: commentId,
124+
content,
125+
author
126+
});
127+
}, []);
128+
129+
// 4. 删除评论的核心方法
130+
const removeComment = useCallback(commentId => {
131+
// 从Yjs数组中删除
132+
yCommentsRef.current.forEach((comment, index) => {
133+
if (comment[0].id === commentId) {
134+
yCommentsRef.current.delete(index, 1);
135+
}
136+
});
201137

202-
// 保存当前选区
203-
const { selection } = editor;
138+
// 从Slate编辑器中删除标记
139+
Editor.removeMark(editor, 'comment');
140+
}, []);
141+
```
204142

205-
// 1. 从 Slate 编辑器中移除评论标记
206-
// 遍历所有文本节点,找到包含指定评论ID的节点并移除标记
207-
const nodesToUpdate = [];
208-
for (const [node, path] of Node.texts(editor)) {
209-
if (node.comment && node.comment.id === commentId) {
210-
nodesToUpdate.push(path);
211-
}
212-
}
143+
关键设计亮点:
213144

214-
// 批量移除评论标记
215-
for (const path of nodesToUpdate) {
216-
try {
217-
// 方法1: 使用 Transforms.unsetNodes 移除 comment 属性
218-
Transforms.unsetNodes(editor, 'comment', {
219-
at: path,
220-
match: n =>
221-
Text.isText(n) && n.comment && n.comment.id === commentId,
222-
});
145+
1. **双重数据同步机制**
223146

224-
// 方法2: 直接操作节点属性(备用方案)
225-
const node = Node.get(editor, path);
226-
if (
227-
Text.isText(node) &&
228-
node.comment &&
229-
node.comment.id === commentId
230-
) {
231-
// 创建新的节点,不包含 comment 属性
232-
const { comment, ...nodeWithoutComment } = node;
233-
Transforms.setNodes(editor, nodeWithoutComment, { at: path });
234-
}
235-
} catch (error) {
236-
console.warn(
237-
'Failed to remove comment from node at path:',
238-
path,
239-
error,
240-
);
241-
}
242-
}
147+
```javascript
148+
// Yjs数据层 (协同)
149+
yCommentsRef.current.push([commentData]);
243150
244-
// 2. 清除编辑器中的活动标记,防止影响后续输入
245-
// 这是关键步骤:确保光标位置不会继承评论标记
246-
if (editor.marks && editor.marks.comment) {
247-
delete editor.marks.comment;
248-
}
151+
// Slate表现层 (渲染)
152+
Editor.addMark(editor, 'comment', commentData);
153+
```
249154

250-
// 强制移除所有评论相关的活动标记
251-
Editor.removeMark(editor, 'comment');
155+
1. **位置漂移解决方案**
252156

253-
// 3. 从 Yjs 评论数组中移除评论数据
254-
// 注意:Yjs 数组中的评论数据结构可能不同,需要检查多种可能的 ID 字段
255-
if (yCommentsRef.current) {
256-
const yCommentsArray = yCommentsRef.current.toArray();
257-
for (let i = yCommentsArray.length - 1; i >= 0; i--) {
258-
const comment = yCommentsArray[i];
259-
// 检查不同可能的 ID 字段和数据结构
260-
const commentToCheck = Array.isArray(comment)
261-
? comment[0]
262-
: comment;
263-
if (
264-
commentToCheck &&
265-
(commentToCheck.id === commentId ||
266-
(commentToCheck.content &&
267-
commentToCheck.author &&
268-
JSON.stringify(commentToCheck).includes(commentId)))
269-
) {
270-
yCommentsRef.current.delete(i, 1);
271-
console.log('Removed comment from Yjs array at index:', i);
272-
break;
273-
}
274-
}
275-
}
157+
```javascript
158+
// 创建相对位置
159+
const pos = Y.createRelativePositionFromTypeIndex(
160+
yTextRef.current, // 关联的Yjs文本类型
161+
charIndex // 原始字符索引
162+
);
276163
277-
// 4. 恢复原始选区并确保清除标记状态
278-
if (selection) {
279-
Transforms.select(editor, selection);
280-
// 再次确保当前选区没有评论标记
281-
Editor.removeMark(editor, 'comment');
282-
}
164+
// 恢复绝对位置
165+
const absPos = Y.createAbsolutePositionFromRelativePosition(
166+
JSON.parse(relativePos),
167+
docRef.current
168+
);
169+
```
283170

284-
// 5. 强制重新规范化编辑器,确保所有标记都被清除
285-
Editor.normalize(editor, { force: true });
171+
1. **高效协同更新**
286172

287-
// 6. 强制触发编辑器重新渲染
288-
editor.onChange();
173+
```javascript
174+
// 监听Yjs数组变化
175+
yCommentsRef.current.observe(() => {
176+
// 触发编辑器重新渲染
177+
setValue(v => [...v]);
178+
});
179+
```
289180

290-
// 7. 延迟再次清除活动标记,确保彻底清除
291-
setTimeout(() => {
292-
if (editor.marks && editor.marks.comment) {
293-
delete editor.marks.comment;
294-
}
295-
Editor.removeMark(editor, 'comment');
296-
}, 0);
181+
这套实现确保了评论功能在多人实时协作时的稳定性和一致性。
297182

298-
console.log('Comment removed successfully');
299-
} catch (error) {
300-
console.error('Error removing comment:', error);
301-
}
302-
},
303-
[editor],
304-
);
305-
```
306-
307183
关键实现说明:
308-
184+
309185
1. **双重定位系统**
310-
186+
311187
- Slate路径定位:用于当前编辑器实例中的快速渲染
312-
- Yjs相对位置:用于跨客户端同步和文档修改时的位置保持
313-
314-
1. **漂移问题解决**
315-
316-
```javascript
317-
// 当文档修改后,通过相对位置可以正确找到评论位置
318-
const absPos = Y.createAbsolutePositionFromRelativePosition(
319-
JSON.parse(comment.start),
320-
docRef.current
321-
);
322-
// absPos包含最新的正确位置信息
323-
```
324-
325-
**主要方法**
326-
188+
- Yjs相对位置:用于跨客户端同步和文档修改时的位置保持主要方法**
189+
327190
- `addComment`:添加评论到指定文本范围
328191
- `removeComment`:删除指定ID的评论
329192
- 评论数据实时同步到所有客户端
330193

331-
332194
**doc-docs(项目介绍文档):**
333195

334-
- 介绍文档具体内容撰写
196+
- 侧边栏目录自动化,介绍文档具体内容撰写
335197
- AI理解文档能力增强:集成 LLM(大语言模型)友好的文档格式(傻瓜ai也能快速读懂的文档)
336198

337199
**doc-web(项目前端):**

0 commit comments

Comments
 (0)