fix: docs 分组与 summary 统一 + 记事本页加「复制链接」按钮

docs 乱:
- main.py 页面路由(/upload /files /wb-admin /wb/{id} /docs /redoc / /health)
  加 tags(pages/docs/meta)+ summary/description,Swagger UI 按 tag 分组显示。
- whiteboard_controller 残留「白板/笔画/修改次数」改为「记事本/文本/编辑次数」。

复制链接:
- 记事本页加「复制链接」按钮,复制 {origin}/wb/{id} 分享链接(保留「复制文本」)。
This commit is contained in:
zikai
2026-07-21 15:04:02 +00:00
parent 655e039aad
commit 374c3d150f
4 changed files with 48 additions and 20 deletions

View File

@@ -1,10 +1,10 @@
"""白板接口REST访问/管理)+ WebSocket实时同步 """白板接口REST访问/管理)+ WebSocket实时同步
路由: 路由:
GET /api/wb/{board_id} 公开:访问白板元数据,不存在则新建(前端 init 用) GET /api/wb/{board_id} 公开:访问记事本元数据,不存在则新建(前端 init 用)
WS /ws/wb/{board_id} 公开:实时协作 + 心跳 WS /ws/wb/{board_id} 公开:实时协作 + 心跳
GET /api/admin/wb Basic Auth管理页列表 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 直接返回静态文件, HTML 页面 /wb/{id} 与管理页 /wb-admin 由 main.py 直接返回静态文件,
不在此 controller 注册,避免与 REST 同路径冲突。 不在此 controller 注册,避免与 REST 同路径冲突。
@@ -41,8 +41,8 @@ def _service(db: Session = Depends(get_db)) -> WhiteboardService:
@router.get( @router.get(
"/api/wb/{board_id}", "/api/wb/{board_id}",
response_model=WhiteboardOut, response_model=WhiteboardOut,
summary="访问白板元数据(不存在则新建)", summary="访问记事本元数据(不存在则新建)",
description="前端打开 /wb/{id} 页面后调本接口拉取初始笔画;不存在时自动创建空板。", description="前端打开 /wb/{id} 页面后调本接口拉取初始文本;不存在时自动创建空板。",
) )
def get_whiteboard(board_id: str, service: WhiteboardService = Depends(_service)) -> WhiteboardOut: def get_whiteboard(board_id: str, service: WhiteboardService = Depends(_service)) -> WhiteboardOut:
return service.get_or_create(board_id) return service.get_or_create(board_id)
@@ -53,8 +53,8 @@ def get_whiteboard(board_id: str, service: WhiteboardService = Depends(_service)
@router.get( @router.get(
"/api/admin/wb", "/api/admin/wb",
response_model=WhiteboardListResponse, response_model=WhiteboardListResponse,
summary="列出所有白板(需鉴权)", summary="列出所有记事本(需鉴权)",
description="白板管理页使用board_id / 创建时间 / 修改次数 / 上次修改时间。", description="记事本管理页使用board_id / 创建时间 / 编辑次数 / 上次修改时间。",
) )
def list_whiteboards( def list_whiteboards(
limit: int = Query(100, ge=1, le=500), limit: int = Query(100, ge=1, le=500),
@@ -68,7 +68,7 @@ def list_whiteboards(
@router.delete( @router.delete(
"/api/admin/wb/{board_id}", "/api/admin/wb/{board_id}",
summary="删除白板(需鉴权)", summary="删除记事本(需鉴权)",
description="删 DB 行,并关闭该 board 的所有在线 WebSocket 连接。", description="删 DB 行,并关闭该 board 的所有在线 WebSocket 连接。",
) )
def delete_whiteboard( def delete_whiteboard(

View File

@@ -155,48 +155,68 @@ def create_app() -> FastAPI:
app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static") app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
# 受 Basic Auth 保护的文档接口 # 受 Basic Auth 保护的文档接口
@app.get("/openapi.json") @app.get("/openapi.json", tags=["docs"], summary="OpenAPI 文档(需鉴权)")
def protected_openapi(_: str = Depends(require_docs_auth)) -> JSONResponse: def protected_openapi(_: str = Depends(require_docs_auth)) -> JSONResponse:
return JSONResponse(app.openapi()) return JSONResponse(app.openapi())
@app.get("/docs") @app.get("/docs", tags=["docs"], summary="Swagger UI需鉴权")
def protected_docs(_: str = Depends(require_docs_auth)): def protected_docs(_: str = Depends(require_docs_auth)):
return get_swagger_ui_html( return get_swagger_ui_html(
openapi_url="/openapi.json", title="zikai docs", swagger_favicon_url="" 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)): def protected_redoc(_: str = Depends(require_docs_auth)):
return get_redoc_html( return get_redoc_html(
openapi_url="/openapi.json", title="zikai docs", redoc_favicon_url="" openapi_url="/openapi.json", title="zikai docs", redoc_favicon_url=""
) )
@app.get("/", response_class=PlainTextResponse) @app.get("/", tags=["meta"], summary="版本号")
def root() -> PlainTextResponse: def root() -> PlainTextResponse:
return PlainTextResponse(f"zikai {app.version}\n") return PlainTextResponse(f"zikai {app.version}\n")
@app.get("/health") @app.get("/health", tags=["meta"], summary="存活探针")
def health() -> dict: def health() -> dict:
return {"status": "ok"} 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: def upload_page() -> HTMLResponse:
"""拖拽 / 多文件 / 分片上传页面(公开,对齐 /api/files/upload"""
return HTMLResponse(render_upload_html()) 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: def files_page(_: str = Depends(require_docs_auth)) -> HTMLResponse:
"""文件浏览页Basic Auth同 docs列出/下载/删除已上传文件。"""
return _serve_static_html("file_browser.html") 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: def whiteboard_admin_page(_: str = Depends(require_docs_auth)) -> HTMLResponse:
"""白板管理页Basic Auth同 docs查看/删除白板。"""
return _serve_static_html("whiteboard_admin.html") 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: def whiteboard_page(board_id: str) -> HTMLResponse:
"""白板页面(公开):访问即协作,不存在则前端拉取时自动新建。"""
return _serve_static_html("whiteboard.html") return _serve_static_html("whiteboard.html")
return app return app

View File

@@ -17,6 +17,7 @@
<span class="wb-online" id="online" title="在线人数"></span> <span class="wb-online" id="online" title="在线人数"></span>
</div> </div>
<div class="wb-bar-right"> <div class="wb-bar-right">
<button class="btn" id="copyLinkBtn" title="复制分享链接">复制链接</button>
<button class="btn" id="copyBtn" title="复制全部文本">复制文本</button> <button class="btn" id="copyBtn" title="复制全部文本">复制文本</button>
<button class="btn danger" id="clearBtn" title="清空全部内容(所有人)">清空</button> <button class="btn danger" id="clearBtn" title="清空全部内容(所有人)">清空</button>
</div> </div>

View File

@@ -17,6 +17,7 @@
const editor = document.getElementById("editor"); const editor = document.getElementById("editor");
const clearBtn = document.getElementById("clearBtn"); const clearBtn = document.getElementById("clearBtn");
const copyBtn = document.getElementById("copyBtn"); const copyBtn = document.getElementById("copyBtn");
const copyLinkBtn = document.getElementById("copyLinkBtn");
const statusEl = document.getElementById("status"); const statusEl = document.getElementById("status");
const onlineEl = document.getElementById("online"); const onlineEl = document.getElementById("online");
@@ -76,6 +77,12 @@
toast(ok ? "已复制全部文本" : "复制失败"); toast(ok ? "已复制全部文本" : "复制失败");
}); });
copyLinkBtn.addEventListener("click", async () => {
const url = `${location.origin}/wb/${boardId}`;
const ok = await copyText(url);
toast(ok ? "链接已复制" : "复制失败");
});
// ---------- 应用远端更新(保留光标) ---------- // ---------- 应用远端更新(保留光标) ----------
// 策略:用最长公共前后缀算出变更区间,仅替换该区间,光标按相对位置调整。 // 策略:用最长公共前后缀算出变更区间,仅替换该区间,光标按相对位置调整。
function applyRemoteUpdate(newText) { function applyRemoteUpdate(newText) {