diff --git a/app/controllers/whiteboard_controller.py b/app/controllers/whiteboard_controller.py index 45347cd..ae840dc 100644 --- a/app/controllers/whiteboard_controller.py +++ b/app/controllers/whiteboard_controller.py @@ -1,10 +1,10 @@ """白板接口:REST(访问/管理)+ WebSocket(实时同步)。 路由: - GET /api/wb/{board_id} 公开:访问白板元数据,不存在则新建(前端 init 用) + GET /api/wb/{board_id} 公开:访问记事本元数据,不存在则新建(前端 init 用) WS /ws/wb/{board_id} 公开:实时协作 + 心跳 GET /api/admin/wb Basic Auth:管理页列表 - DELETE /api/admin/wb/{board_id} Basic Auth:删除白板 + DELETE /api/admin/wb/{board_id} Basic Auth:删除记事本 HTML 页面 /wb/{id} 与管理页 /wb-admin 由 main.py 直接返回静态文件, 不在此 controller 注册,避免与 REST 同路径冲突。 @@ -41,8 +41,8 @@ def _service(db: Session = Depends(get_db)) -> WhiteboardService: @router.get( "/api/wb/{board_id}", response_model=WhiteboardOut, - summary="访问白板元数据(不存在则新建)", - description="前端打开 /wb/{id} 页面后调本接口拉取初始笔画;不存在时自动创建空板。", + summary="访问记事本元数据(不存在则新建)", + description="前端打开 /wb/{id} 页面后调本接口拉取初始文本;不存在时自动创建空板。", ) def get_whiteboard(board_id: str, service: WhiteboardService = Depends(_service)) -> WhiteboardOut: return service.get_or_create(board_id) @@ -53,8 +53,8 @@ def get_whiteboard(board_id: str, service: WhiteboardService = Depends(_service) @router.get( "/api/admin/wb", response_model=WhiteboardListResponse, - summary="列出所有白板(需鉴权)", - description="供白板管理页使用:board_id / 创建时间 / 修改次数 / 上次修改时间。", + summary="列出所有记事本(需鉴权)", + description="供记事本管理页使用:board_id / 创建时间 / 编辑次数 / 上次修改时间。", ) def list_whiteboards( limit: int = Query(100, ge=1, le=500), @@ -68,7 +68,7 @@ def list_whiteboards( @router.delete( "/api/admin/wb/{board_id}", - summary="删除白板(需鉴权)", + summary="删除记事本(需鉴权)", description="删 DB 行,并关闭该 board 的所有在线 WebSocket 连接。", ) def delete_whiteboard( diff --git a/app/main.py b/app/main.py index 7eb3573..0fbf145 100644 --- a/app/main.py +++ b/app/main.py @@ -155,48 +155,68 @@ def create_app() -> FastAPI: app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static") # 受 Basic Auth 保护的文档接口 - @app.get("/openapi.json") + @app.get("/openapi.json", tags=["docs"], summary="OpenAPI 文档(需鉴权)") def protected_openapi(_: str = Depends(require_docs_auth)) -> JSONResponse: return JSONResponse(app.openapi()) - @app.get("/docs") + @app.get("/docs", tags=["docs"], summary="Swagger UI(需鉴权)") def protected_docs(_: str = Depends(require_docs_auth)): return get_swagger_ui_html( openapi_url="/openapi.json", title="zikai docs", swagger_favicon_url="" ) - @app.get("/redoc") + @app.get("/redoc", tags=["docs"], summary="ReDoc(需鉴权)") def protected_redoc(_: str = Depends(require_docs_auth)): return get_redoc_html( openapi_url="/openapi.json", title="zikai docs", redoc_favicon_url="" ) - @app.get("/", response_class=PlainTextResponse) + @app.get("/", tags=["meta"], summary="版本号") def root() -> PlainTextResponse: return PlainTextResponse(f"zikai {app.version}\n") - @app.get("/health") + @app.get("/health", tags=["meta"], summary="存活探针") def health() -> dict: return {"status": "ok"} - @app.get("/upload", response_class=HTMLResponse) + @app.get( + "/upload", + response_class=HTMLResponse, + tags=["pages"], + summary="上传页面", + description="拖拽 / 多文件 / 分片(4 MiB) / 断点续传上传页面(公开)。", + ) def upload_page() -> HTMLResponse: - """拖拽 / 多文件 / 分片上传页面(公开,对齐 /api/files/upload)。""" return HTMLResponse(render_upload_html()) - @app.get("/files", response_class=HTMLResponse) + @app.get( + "/files", + response_class=HTMLResponse, + tags=["pages"], + summary="文件浏览页(需鉴权)", + description="列出 / 下载 / 删除已上传文件;支持多选、批量下载删除与分页。Basic Auth 同 docs。", + ) def files_page(_: str = Depends(require_docs_auth)) -> HTMLResponse: - """文件浏览页(Basic Auth,同 docs):列出/下载/删除已上传文件。""" return _serve_static_html("file_browser.html") - @app.get("/wb-admin", response_class=HTMLResponse) + @app.get( + "/wb-admin", + response_class=HTMLResponse, + tags=["pages"], + summary="记事本管理页(需鉴权)", + description="查看所有记事本的创建时间 / 编辑次数 / 上次修改时间,并可删除。Basic Auth 同 docs。", + ) def whiteboard_admin_page(_: str = Depends(require_docs_auth)) -> HTMLResponse: - """白板管理页(Basic Auth,同 docs):查看/删除白板。""" return _serve_static_html("whiteboard_admin.html") - @app.get("/wb/{board_id}", response_class=HTMLResponse) + @app.get( + "/wb/{board_id}", + response_class=HTMLResponse, + tags=["pages"], + summary="记事本页面", + description="公开访问的共享文本记事本,不存在则自动新建;实时协作走 WS /ws/wb/{id}。", + ) def whiteboard_page(board_id: str) -> HTMLResponse: - """白板页面(公开):访问即协作,不存在则前端拉取时自动新建。""" return _serve_static_html("whiteboard.html") return app diff --git a/static/whiteboard.html b/static/whiteboard.html index 4cbacda..dfb95c0 100644 --- a/static/whiteboard.html +++ b/static/whiteboard.html @@ -17,6 +17,7 @@
+
diff --git a/static/whiteboard.js b/static/whiteboard.js index eff2d7f..daacd13 100644 --- a/static/whiteboard.js +++ b/static/whiteboard.js @@ -17,6 +17,7 @@ const editor = document.getElementById("editor"); const clearBtn = document.getElementById("clearBtn"); const copyBtn = document.getElementById("copyBtn"); + const copyLinkBtn = document.getElementById("copyLinkBtn"); const statusEl = document.getElementById("status"); const onlineEl = document.getElementById("online"); @@ -76,6 +77,12 @@ toast(ok ? "已复制全部文本" : "复制失败"); }); + copyLinkBtn.addEventListener("click", async () => { + const url = `${location.origin}/wb/${boardId}`; + const ok = await copyText(url); + toast(ok ? "链接已复制" : "复制失败"); + }); + // ---------- 应用远端更新(保留光标) ---------- // 策略:用最长公共前后缀算出变更区间,仅替换该区间,光标按相对位置调整。 function applyRemoteUpdate(newText) {