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:
@@ -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(
|
||||||
|
|||||||
46
app/main.py
46
app/main.py
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user