Files
Uni-Lab-OS/unilabos/labware_manager/app.py
ALITTLELZ 2fd4270831 添加 PRCXI 耗材管理 Web 应用 (labware_manager)
新增 labware_manager 模块:
- Web UI 支持耗材 CRUD、SVG 俯视图/侧面图实时预览
- SVG 支持触控板双指缩放(pinch-to-zoom)和平移
- 网格排列自动居中按钮(autoCenter)
- 表单参数标签中英文双语显示
- 从已有代码/YAML 导入、Python/YAML 代码生成

更新 CLAUDE.md:补充 labware manager、decorator 注册模式、CI 说明

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 15:19:52 +08:00

197 lines
5.5 KiB
Python

"""FastAPI 应用 + CRUD API + 启动入口。
用法: python -m unilabos.labware_manager.app
"""
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import List, Optional
from fastapi import FastAPI, HTTPException, Query, Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from unilabos.labware_manager.models import LabwareDB, LabwareItem
_HERE = Path(__file__).resolve().parent
_DB_PATH = _HERE / "labware_db.json"
app = FastAPI(title="PRCXI 耗材管理", version="1.0")
# 静态文件 + 模板
app.mount("/static", StaticFiles(directory=str(_HERE / "static")), name="static")
templates = Jinja2Templates(directory=str(_HERE / "templates"))
# ---------- DB 读写 ----------
def _load_db() -> LabwareDB:
if not _DB_PATH.exists():
return LabwareDB()
with open(_DB_PATH, "r", encoding="utf-8") as f:
return LabwareDB(**json.load(f))
def _save_db(db: LabwareDB) -> None:
with open(_DB_PATH, "w", encoding="utf-8") as f:
json.dump(db.model_dump(), f, ensure_ascii=False, indent=2)
# ---------- 页面路由 ----------
@app.get("/", response_class=HTMLResponse)
async def index_page(request: Request):
db = _load_db()
# 按 type 分组
groups = {}
for item in db.items:
groups.setdefault(item.type, []).append(item)
return templates.TemplateResponse("index.html", {
"request": request,
"groups": groups,
"total": len(db.items),
})
@app.get("/labware/new", response_class=HTMLResponse)
async def new_page(request: Request, type: str = "plate"):
return templates.TemplateResponse("edit.html", {
"request": request,
"item": None,
"labware_type": type,
"is_new": True,
})
@app.get("/labware/{item_id}", response_class=HTMLResponse)
async def detail_page(request: Request, item_id: str):
db = _load_db()
item = _find_item(db, item_id)
if not item:
raise HTTPException(404, "耗材不存在")
return templates.TemplateResponse("detail.html", {
"request": request,
"item": item,
})
@app.get("/labware/{item_id}/edit", response_class=HTMLResponse)
async def edit_page(request: Request, item_id: str):
db = _load_db()
item = _find_item(db, item_id)
if not item:
raise HTTPException(404, "耗材不存在")
return templates.TemplateResponse("edit.html", {
"request": request,
"item": item,
"labware_type": item.type,
"is_new": False,
})
# ---------- API 端点 ----------
@app.get("/api/labware")
async def api_list_labware():
db = _load_db()
return {"items": [item.model_dump() for item in db.items]}
@app.post("/api/labware")
async def api_create_labware(request: Request):
data = await request.json()
db = _load_db()
item = LabwareItem(**data)
# 确保 id 唯一
existing_ids = {it.id for it in db.items}
while item.id in existing_ids:
import uuid
item.id = uuid.uuid4().hex[:8]
db.items.append(item)
_save_db(db)
return {"status": "ok", "id": item.id}
@app.put("/api/labware/{item_id}")
async def api_update_labware(item_id: str, request: Request):
data = await request.json()
db = _load_db()
for i, it in enumerate(db.items):
if it.id == item_id or it.function_name == item_id:
updated = LabwareItem(**{**it.model_dump(), **data, "id": it.id})
db.items[i] = updated
_save_db(db)
return {"status": "ok", "id": it.id}
raise HTTPException(404, "耗材不存在")
@app.delete("/api/labware/{item_id}")
async def api_delete_labware(item_id: str):
db = _load_db()
original_len = len(db.items)
db.items = [it for it in db.items if it.id != item_id and it.function_name != item_id]
if len(db.items) == original_len:
raise HTTPException(404, "耗材不存在")
_save_db(db)
return {"status": "ok"}
@app.post("/api/generate-code")
async def api_generate_code(request: Request):
body = await request.json() if await request.body() else {}
test_mode = body.get("test_mode", True)
db = _load_db()
if not db.items:
raise HTTPException(400, "数据库为空,请先导入")
from unilabos.labware_manager.codegen import generate_code
from unilabos.labware_manager.yaml_gen import generate_yaml
py_path = generate_code(db, test_mode=test_mode)
yaml_paths = generate_yaml(db, test_mode=test_mode)
return {
"status": "ok",
"python_file": str(py_path),
"yaml_files": [str(p) for p in yaml_paths],
"test_mode": test_mode,
}
@app.post("/api/import-from-code")
async def api_import_from_code():
from unilabos.labware_manager.importer import import_from_code, save_db
db = import_from_code()
save_db(db)
return {
"status": "ok",
"count": len(db.items),
"items": [{"function_name": it.function_name, "type": it.type} for it in db.items],
}
# ---------- 辅助函数 ----------
def _find_item(db: LabwareDB, item_id: str) -> Optional[LabwareItem]:
for item in db.items:
if item.id == item_id or item.function_name == item_id:
return item
return None
# ---------- 启动入口 ----------
def main():
import uvicorn
port = int(os.environ.get("LABWARE_PORT", "8010"))
print(f"PRCXI 耗材管理 → http://localhost:{port}")
uvicorn.run(app, host="0.0.0.0", port=port)
if __name__ == "__main__":
main()