ux refine

This commit is contained in:
2026-04-19 13:31:17 +02:00
parent 4c4ff61fab
commit 314fc16b98
5 changed files with 139 additions and 10 deletions
+18 -2
View File
@@ -82,6 +82,10 @@ def _image_response_or_404(target) -> Response:
return Response(content=target.image_blob, media_type=target.image_mime_type) return Response(content=target.image_blob, media_type=target.image_mime_type)
def _wants_add_next(submit_action: str | None) -> bool:
return submit_action == "save_and_add_next"
def _build_search_results(db: Session, query: str) -> list[dict]: def _build_search_results(db: Session, query: str) -> list[dict]:
keyword = f"%{query.lower()}%" keyword = f"%{query.lower()}%"
results: list[dict] = [] results: list[dict] = []
@@ -333,6 +337,7 @@ def create_app() -> FastAPI:
note: str | None = Form(default=None), note: str | None = Form(default=None),
quantity: str | None = Form(default=None), quantity: str | None = Form(default=None),
is_container: str | None = Form(default=None), is_container: str | None = Form(default=None),
submit_action: str | None = Form(default=None),
image_file: UploadFile | None = File(default=None), image_file: UploadFile | None = File(default=None),
db: Session = Depends(get_db), db: Session = Depends(get_db),
) -> RedirectResponse: ) -> RedirectResponse:
@@ -348,7 +353,13 @@ def create_app() -> FastAPI:
db.add(item) db.add(item)
db.commit() db.commit()
db.refresh(item) db.refresh(item)
return RedirectResponse(url=f"/items/{item.id}", status_code=status.HTTP_303_SEE_OTHER) if _wants_add_next(submit_action):
redirect_url = f"/boxes/{box.id}/items/new"
elif item.is_container:
redirect_url = f"/items/{item.id}"
else:
redirect_url = f"/boxes/{box.id}"
return RedirectResponse(url=redirect_url, status_code=status.HTTP_303_SEE_OTHER)
@app.get("/items/{item_id}") @app.get("/items/{item_id}")
def show_item(item_id: int, request: Request, db: Session = Depends(get_db)): def show_item(item_id: int, request: Request, db: Session = Depends(get_db)):
@@ -436,6 +447,7 @@ def create_app() -> FastAPI:
name: str = Form(...), name: str = Form(...),
note: str | None = Form(default=None), note: str | None = Form(default=None),
quantity: str | None = Form(default=None), quantity: str | None = Form(default=None),
submit_action: str | None = Form(default=None),
image_file: UploadFile | None = File(default=None), image_file: UploadFile | None = File(default=None),
db: Session = Depends(get_db), db: Session = Depends(get_db),
) -> RedirectResponse: ) -> RedirectResponse:
@@ -451,7 +463,11 @@ def create_app() -> FastAPI:
db.add(subitem) db.add(subitem)
db.commit() db.commit()
db.refresh(subitem) db.refresh(subitem)
return RedirectResponse(url=f"/items/{item.id}", status_code=status.HTTP_303_SEE_OTHER) if _wants_add_next(submit_action):
redirect_url = f"/items/{item.id}/subitems/new"
else:
redirect_url = f"/items/{item.id}"
return RedirectResponse(url=redirect_url, status_code=status.HTTP_303_SEE_OTHER)
@app.get("/subitems/{subitem_id}/image") @app.get("/subitems/{subitem_id}/image")
def get_subitem_image(subitem_id: int, db: Session = Depends(get_db)) -> Response: def get_subitem_image(subitem_id: int, db: Session = Depends(get_db)) -> Response:
+12
View File
@@ -63,6 +63,12 @@ button,
padding: 10px 14px; padding: 10px 14px;
} }
.button-secondary {
background: #eef3f8;
color: #1f2937;
border: 1px solid #cbd5e1;
}
.button:hover, .button:hover,
button:hover { button:hover {
opacity: 0.92; opacity: 0.92;
@@ -280,6 +286,12 @@ button:hover {
gap: 14px; gap: 14px;
} }
.form-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.form-field { .form-field {
margin: 0; margin: 0;
} }
+8 -1
View File
@@ -77,6 +77,13 @@
</button> </button>
</section> </section>
{% endif %} {% endif %}
<button type="submit">{{ submit_label }}</button> <div class="form-actions">
<button type="submit" name="submit_action" value="save">{{ submit_label }}</button>
{% if not item %}
<button type="submit" name="submit_action" value="save_and_add_next" class="button-secondary">
保存并添加下一个
</button>
{% endif %}
</div>
</form> </form>
{% endblock %} {% endblock %}
+8 -1
View File
@@ -72,6 +72,13 @@
</button> </button>
</section> </section>
{% endif %} {% endif %}
<button type="submit">{{ submit_label }}</button> <div class="form-actions">
<button type="submit" name="submit_action" value="save">{{ submit_label }}</button>
{% if not subitem %}
<button type="submit" name="submit_action" value="save_and_add_next" class="button-secondary">
保存并添加下一个
</button>
{% endif %}
</div>
</form> </form>
{% endblock %} {% endblock %}
+93 -6
View File
@@ -21,19 +21,28 @@ def create_item(
quantity="2", quantity="2",
is_container=False, is_container=False,
image=None, image=None,
submit_action="save",
): ):
data = {"name": name, "note": note, "quantity": quantity} data = {"name": name, "note": note, "quantity": quantity, "submit_action": submit_action}
if is_container: if is_container:
data["is_container"] = "on" data["is_container"] = "on"
files = {"image_file": image} if image is not None else None files = {"image_file": image} if image is not None else None
return client.post(f"/boxes/{box_id}/items", data=data, files=files, follow_redirects=False) return client.post(f"/boxes/{box_id}/items", data=data, files=files, follow_redirects=False)
def create_subitem(client, item_id, name="SubItem A", note="Small", quantity="3", image=None): def create_subitem(
client,
item_id,
name="SubItem A",
note="Small",
quantity="3",
image=None,
submit_action="save",
):
files = {"image_file": image} if image is not None else None files = {"image_file": image} if image is not None else None
return client.post( return client.post(
f"/items/{item_id}/subitems", f"/items/{item_id}/subitems",
data={"name": name, "note": note, "quantity": quantity}, data={"name": name, "note": note, "quantity": quantity, "submit_action": submit_action},
files=files, files=files,
follow_redirects=False, follow_redirects=False,
) )
@@ -320,14 +329,13 @@ def test_post_redirects_are_reasonable(client, db_session):
db_session.commit() db_session.commit()
item_response = create_item(client, box.id, name="Lamp") item_response = create_item(client, box.id, name="Lamp")
item_id = int(item_response.headers["location"].split("/")[-1]) item = db_session.query(Item).one()
item = db_session.get(Item, item_id)
item.is_container = True item.is_container = True
db_session.commit() db_session.commit()
subitem_response = create_subitem(client, item.id, name="Bulb") subitem_response = create_subitem(client, item.id, name="Bulb")
assert item_response.headers["location"] == f"/items/{item.id}" assert item_response.headers["location"] == f"/boxes/{box.id}"
assert subitem_response.headers["location"] == f"/items/{item.id}" assert subitem_response.headers["location"] == f"/items/{item.id}"
@@ -730,6 +738,7 @@ def test_new_item_page_shows_clear_context_and_default_quantity(client, db_sessi
assert 'name="quantity"' in response.text assert 'name="quantity"' in response.text
assert 'value="1"' in response.text assert 'value="1"' in response.text
assert "这个物品本身是一个小容器" in response.text assert "这个物品本身是一个小容器" in response.text
assert "保存并添加下一个" in response.text
def test_new_subitem_page_shows_clear_context_and_default_quantity(client, db_session): def test_new_subitem_page_shows_clear_context_and_default_quantity(client, db_session):
@@ -746,6 +755,7 @@ def test_new_subitem_page_shows_clear_context_and_default_quantity(client, db_se
assert "文件袋" in response.text assert "文件袋" in response.text
assert 'name="quantity"' in response.text assert 'name="quantity"' in response.text
assert 'value="1"' in response.text assert 'value="1"' in response.text
assert "保存并添加下一个" in response.text
def test_box_detail_page_renders_clear_hierarchy_and_dense_list_structure(client, db_session): def test_box_detail_page_renders_clear_hierarchy_and_dense_list_structure(client, db_session):
@@ -775,3 +785,80 @@ def test_item_detail_page_renders_clear_hierarchy(client, db_session):
assert "容器型 Item" in response.text assert "容器型 Item" in response.text
assert "书房箱" in response.text assert "书房箱" in response.text
assert "SubItem" in response.text assert "SubItem" in response.text
def test_creating_regular_item_redirects_back_to_parent_box(client, db_session):
box = Box(name="连续录入箱")
db_session.add(box)
db_session.commit()
response = create_item(client, box.id, name="剪刀", is_container=False)
assert response.status_code == 303
assert response.headers["location"] == f"/boxes/{box.id}"
def test_creating_regular_subitem_redirects_back_to_parent_item(client, db_session):
box = Box(name="配件箱")
item = Item(name="线材袋", box=box, is_container=True)
db_session.add_all([box, item])
db_session.commit()
response = create_subitem(client, item.id, name="USB头")
assert response.status_code == 303
assert response.headers["location"] == f"/items/{item.id}"
def test_creating_box_redirects_to_new_box_detail(client):
response = create_box(client, name="新箱子")
assert response.status_code == 303
assert response.headers["location"].startswith("/boxes/")
assert not response.headers["location"].endswith("/items/new")
def test_creating_container_item_redirects_to_item_detail(client, db_session):
box = Box(name="子容器箱")
db_session.add(box)
db_session.commit()
response = create_item(client, box.id, name="小收纳盒", is_container=True)
created_item = db_session.query(Item).one()
assert response.status_code == 303
assert response.headers["location"] == f"/items/{created_item.id}"
def test_creating_item_with_save_and_add_next_returns_to_same_new_item_context(client, db_session):
box = Box(name="快速录入箱")
db_session.add(box)
db_session.commit()
response = create_item(
client,
box.id,
name="袜子",
is_container=False,
submit_action="save_and_add_next",
)
assert response.status_code == 303
assert response.headers["location"] == f"/boxes/{box.id}/items/new"
def test_creating_subitem_with_save_and_add_next_returns_to_same_new_subitem_context(client, db_session):
box = Box(name="电子箱")
item = Item(name="配件袋", box=box, is_container=True)
db_session.add_all([box, item])
db_session.commit()
response = create_subitem(
client,
item.id,
name="转接头",
submit_action="save_and_add_next",
)
assert response.status_code == 303
assert response.headers["location"] == f"/items/{item.id}/subitems/new"