refactor: 白板从画笔画板改为文本记事本
原实现是 Canvas 画笔画板,与「文字白板/记事本」需求不符。重做为纯文本实时协作: - model:strokes JSON -> content TEXT + version INT(乐观锁)+ edit_count; schema/dao/service 同步重构,append_stroke/replace_strokes -> update_content。 - WS 协议:stroke -> edit(发完整文本,debounce 400ms);init 下发 content/version。 update 帧广播给他人,cleared 广播给所有人。 - 前端:canvas -> textarea;收到远端 update 用最长公共前后缀算变更区间, 仅替换该区间并保留光标(区间前不动/后平移/内移末尾);清空/复制文本按钮。 - schema.sql 更新 whiteboard 表 DDL;DB 旧表 DROP 重建(开发环境)。 - 测试脚本与 README 同步更新。
This commit is contained in:
@@ -86,18 +86,18 @@ def delete_whiteboard(
|
||||
|
||||
@router.websocket("/ws/wb/{board_id}")
|
||||
async def whiteboard_ws(websocket: WebSocket, board_id: str) -> None:
|
||||
"""白板实时协作端点。
|
||||
"""白板实时协作端点(文本记事本)。
|
||||
|
||||
协议(JSON 文本帧):
|
||||
client -> server:
|
||||
{"type":"hello","client_id":"..."} 首帧(可选,未带则服务端生成)
|
||||
{"type":"ping"} 心跳,服务端回 pong 并刷新计时
|
||||
{"type":"stroke","stroke":{...}} 新增笔画,持久化并广播给他人
|
||||
{"type":"edit","content":"..."} debounce 后发完整文本,持久化并广播给他人
|
||||
{"type":"clear"} 清空,持久化并广播给所有人
|
||||
server -> client:
|
||||
{"type":"init","strokes":[...],"stroke_count":n}
|
||||
{"type":"init","content":"...","version":n,"edit_count":m}
|
||||
{"type":"pong"}
|
||||
{"type":"stroke","stroke":{...},"client_id":"..."}
|
||||
{"type":"update","content":"...","version":n,"client_id":"..."} 文本变更
|
||||
{"type":"cleared","client_id":"..."}
|
||||
{"type":"error","msg":"..."}
|
||||
"""
|
||||
@@ -115,7 +115,6 @@ async def whiteboard_ws(websocket: WebSocket, board_id: str) -> None:
|
||||
client_id = _extract_client_id(first) or uuid.uuid4().hex[:12]
|
||||
|
||||
# 校验 board_id 并加载白板(不存在则新建)
|
||||
from ..database import get_session_local
|
||||
try:
|
||||
board = _with_db(lambda db: WhiteboardService(WhiteboardDAO(db), hub=hub).get_or_create(board_id))
|
||||
except HTTPException as exc:
|
||||
@@ -128,8 +127,9 @@ async def whiteboard_ws(websocket: WebSocket, board_id: str) -> None:
|
||||
await hub.register(conn)
|
||||
await _safe_send(websocket, {
|
||||
"type": "init",
|
||||
"strokes": board.strokes,
|
||||
"stroke_count": board.stroke_count,
|
||||
"content": board.content,
|
||||
"version": board.version,
|
||||
"edit_count": board.edit_count,
|
||||
})
|
||||
|
||||
# 主循环:收消息 -> 处理 -> 广播
|
||||
@@ -146,17 +146,22 @@ async def whiteboard_ws(websocket: WebSocket, board_id: str) -> None:
|
||||
continue
|
||||
# 任何有效业务帧都视为活性证据
|
||||
conn.touch()
|
||||
if mtype == "stroke":
|
||||
stroke = msg.get("stroke") or {}
|
||||
if mtype == "edit":
|
||||
content = msg.get("content")
|
||||
if not isinstance(content, str):
|
||||
await _safe_send(websocket, {"type": "error", "msg": "content 必须是字符串"})
|
||||
continue
|
||||
try:
|
||||
_with_db(lambda db: WhiteboardService(WhiteboardDAO(db), hub=hub).append_stroke(board_id, stroke))
|
||||
out = _with_db(
|
||||
lambda db: WhiteboardService(WhiteboardDAO(db), hub=hub).update_content(board_id, content)
|
||||
)
|
||||
except HTTPException as exc:
|
||||
await _safe_send(websocket, {"type": "error", "msg": exc.detail})
|
||||
continue
|
||||
# 广播给他人(发送者本地已画,不回推)
|
||||
# 广播给他人(发送者本地已更新,不回推)
|
||||
await hub.broadcast(
|
||||
board_id,
|
||||
{"type": "stroke", "stroke": stroke, "client_id": client_id},
|
||||
{"type": "update", "content": out.content, "version": out.version, "client_id": client_id},
|
||||
exclude=conn,
|
||||
)
|
||||
elif mtype == "clear":
|
||||
|
||||
Reference in New Issue
Block a user