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实时同步
路由:
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(

View File

@@ -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

View File

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

View File

@@ -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) {