Compare commits
5 Commits
22ea44d8cd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| facf82c898 | |||
| d24c41d05f | |||
| 4955c87d78 | |||
| bfa554b407 | |||
| e5fee32098 |
@@ -14,3 +14,4 @@ data/*.db
|
||||
**/.Spotlight-V100
|
||||
.Trashes
|
||||
**/.Trashes
|
||||
.codex
|
||||
+31
-1
@@ -88,6 +88,35 @@ def _wants_add_next(submit_action: str | None) -> bool:
|
||||
return submit_action == "save_and_add_next"
|
||||
|
||||
|
||||
def _format_average(total: int, divisor: int) -> str:
|
||||
if divisor == 0:
|
||||
return "0.0"
|
||||
return f"{total / divisor:.1f}"
|
||||
|
||||
|
||||
def _build_boxes_overview_summary(db: Session) -> dict[str, int | str]:
|
||||
box_count = db.query(func.count(Box.id)).scalar() or 0
|
||||
item_count = db.query(func.count(Item.id)).scalar() or 0
|
||||
subitem_count = db.query(func.count(SubItem.id)).scalar() or 0
|
||||
container_item_count = (
|
||||
db.query(func.count(Item.id))
|
||||
.filter(Item.is_container.is_(True))
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
return {
|
||||
"box_count": box_count,
|
||||
"item_count": item_count,
|
||||
"item_and_subitem_count": item_count + subitem_count,
|
||||
"avg_items_per_box": _format_average(item_count, box_count),
|
||||
"avg_subitems_per_container_item": _format_average(
|
||||
subitem_count,
|
||||
container_item_count,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _build_search_results(db: Session, query: str) -> list[dict]:
|
||||
keyword = f"%{query.lower()}%"
|
||||
results: list[dict] = []
|
||||
@@ -231,10 +260,11 @@ def create_app() -> FastAPI:
|
||||
@app.get("/boxes")
|
||||
def list_boxes(request: Request, db: Session = Depends(get_db)):
|
||||
boxes = db.query(Box).order_by(Box.id.desc()).all()
|
||||
summary = _build_boxes_overview_summary(db)
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="boxes/index.html",
|
||||
context={"page_title": "箱子", "boxes": boxes},
|
||||
context={"page_title": "箱子", "boxes": boxes, "summary": summary},
|
||||
)
|
||||
|
||||
@app.get("/boxes/new")
|
||||
|
||||
@@ -231,6 +231,25 @@ button:focus-visible {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.summary-block {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.summary-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.summary-list li + li {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.compact-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
|
||||
@@ -15,6 +15,22 @@
|
||||
<a class="button button-primary" href="/boxes/new">新建箱子</a>
|
||||
</div>
|
||||
|
||||
<section class="stack summary-section">
|
||||
<div class="section-heading">
|
||||
<h2>当前概览</h2>
|
||||
<p class="muted">快速查看当前装箱记录的核心统计。</p>
|
||||
</div>
|
||||
<section class="card summary-block">
|
||||
<ul class="summary-list">
|
||||
<li><strong>箱子总数:</strong>{{ summary.box_count }}</li>
|
||||
<li><strong>物品总数(不含子物品):</strong>{{ summary.item_count }}</li>
|
||||
<li><strong>物品总数(含子物品):</strong>{{ summary.item_and_subitem_count }}</li>
|
||||
<li><strong>平均每箱物品数:</strong>{{ summary.avg_items_per_box }}</li>
|
||||
<li><strong>平均每个容器型 Item 的子物品数:</strong>{{ summary.avg_subitems_per_container_item }}</li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{% if boxes %}
|
||||
<div class="overview-grid">
|
||||
{% for box in boxes %}
|
||||
|
||||
@@ -44,21 +44,6 @@
|
||||
名称
|
||||
<input type="text" name="name" value="{{ item.name if item else '' }}" required autofocus>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
数量
|
||||
<input type="number" name="quantity" min="0" value="{{ item.quantity if item and item.quantity is not none else '1' }}">
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="is_container" {% if item and item.is_container %}checked{% endif %}>
|
||||
这个物品本身是一个小容器
|
||||
</label>
|
||||
<div class="checkbox-help">
|
||||
勾选后,这个 Item 将作为“第二层容器”,后续可以继续往里面添加最后一级的 SubItem。
|
||||
</div>
|
||||
<label class="form-field">
|
||||
备注
|
||||
<textarea name="note" rows="4">{{ item.note if item and item.note else '' }}</textarea>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
图片
|
||||
<input type="file" name="image_file" accept="image/*">
|
||||
@@ -77,6 +62,21 @@
|
||||
</button>
|
||||
</section>
|
||||
{% endif %}
|
||||
<label class="form-field">
|
||||
数量
|
||||
<input type="number" name="quantity" min="0" value="{{ item.quantity if item and item.quantity is not none else '1' }}">
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="is_container" {% if item and item.is_container %}checked{% endif %}>
|
||||
这个物品本身是一个小容器
|
||||
</label>
|
||||
<div class="checkbox-help">
|
||||
勾选后,这个 Item 将作为“第二层容器”,后续可以继续往里面添加最后一级的 SubItem。
|
||||
</div>
|
||||
<label class="form-field">
|
||||
备注
|
||||
<textarea name="note" rows="4">{{ item.note if item and item.note else '' }}</textarea>
|
||||
</label>
|
||||
<div class="form-actions">
|
||||
<button type="submit" name="submit_action" value="save" class="button button-primary">{{ submit_label }}</button>
|
||||
{% if not item %}
|
||||
|
||||
@@ -70,6 +70,11 @@
|
||||
<span>上级容器:{{ item.name }}</span>
|
||||
</div>
|
||||
{% if subitem.note %}<p class="row-note">备注:{{ subitem.note }}</p>{% endif %}
|
||||
<div class="actions">
|
||||
<form method="post" action="/subitems/{{ subitem.id }}/delete">
|
||||
<button type="submit" class="button button-danger button-small">删除子物品</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
|
||||
@@ -46,14 +46,6 @@
|
||||
名称
|
||||
<input type="text" name="name" value="{{ subitem.name if subitem else '' }}" required autofocus>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
数量
|
||||
<input type="number" name="quantity" min="0" value="{{ subitem.quantity if subitem and subitem.quantity is not none else '1' }}">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
备注
|
||||
<textarea name="note" rows="4">{{ subitem.note if subitem and subitem.note else '' }}</textarea>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
图片
|
||||
<input type="file" name="image_file" accept="image/*">
|
||||
@@ -72,6 +64,14 @@
|
||||
</button>
|
||||
</section>
|
||||
{% endif %}
|
||||
<label class="form-field">
|
||||
数量
|
||||
<input type="number" name="quantity" min="0" value="{{ subitem.quantity if subitem and subitem.quantity is not none else '1' }}">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
备注
|
||||
<textarea name="note" rows="4">{{ subitem.note if subitem and subitem.note else '' }}</textarea>
|
||||
</label>
|
||||
<div class="form-actions">
|
||||
<button type="submit" name="submit_action" value="save" class="button button-primary">{{ submit_label }}</button>
|
||||
{% if not subitem %}
|
||||
|
||||
@@ -178,6 +178,53 @@ def test_boxes_overview_renders_cleanly_when_note_is_empty(client, db_session):
|
||||
assert "状态:" not in response.text
|
||||
|
||||
|
||||
def test_boxes_overview_summary_shows_expected_counts_and_averages(client, db_session):
|
||||
first_box = Box(name="卧室箱")
|
||||
second_box = Box(name="书房箱")
|
||||
regular_item = Item(name="书", box=first_box, is_container=False)
|
||||
container_item = Item(name="配件袋", box=first_box, is_container=True)
|
||||
second_container_item = Item(name="文件袋", box=second_box, is_container=True)
|
||||
db_session.add_all(
|
||||
[
|
||||
first_box,
|
||||
second_box,
|
||||
regular_item,
|
||||
container_item,
|
||||
second_container_item,
|
||||
SubItem(name="转接头", parent_item=container_item),
|
||||
SubItem(name="数据线", parent_item=container_item),
|
||||
]
|
||||
)
|
||||
db_session.commit()
|
||||
|
||||
response = client.get("/boxes")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "当前概览" in response.text
|
||||
assert "箱子总数" in response.text
|
||||
assert "物品总数(不含子物品)" in response.text
|
||||
assert "物品总数(含子物品)" in response.text
|
||||
assert "平均每箱物品数" in response.text
|
||||
assert "平均每个容器型 Item 的子物品数" in response.text
|
||||
assert "箱子总数:</strong>2" in response.text
|
||||
assert "物品总数(不含子物品):</strong>3" in response.text
|
||||
assert "物品总数(含子物品):</strong>5" in response.text
|
||||
assert "平均每箱物品数:</strong>1.5" in response.text
|
||||
assert "平均每个容器型 Item 的子物品数:</strong>1.0" in response.text
|
||||
|
||||
|
||||
def test_boxes_overview_summary_handles_empty_data_safely(client):
|
||||
response = client.get("/boxes")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "当前概览" in response.text
|
||||
assert "箱子总数:</strong>0" in response.text
|
||||
assert "物品总数(不含子物品):</strong>0" in response.text
|
||||
assert "物品总数(含子物品):</strong>0" in response.text
|
||||
assert "平均每箱物品数:</strong>0.0" in response.text
|
||||
assert "平均每个容器型 Item 的子物品数:</strong>0.0" in response.text
|
||||
|
||||
|
||||
def test_can_create_box(client, db_session):
|
||||
response = create_box(client, name="Kitchen Box")
|
||||
|
||||
@@ -875,6 +922,7 @@ def test_new_item_page_shows_clear_context_and_default_quantity(client, db_sessi
|
||||
assert "主卧箱" in response.text
|
||||
assert 'name="quantity"' in response.text
|
||||
assert 'value="1"' in response.text
|
||||
assert response.text.index('name="name"') < response.text.index('name="image_file"') < response.text.index('name="quantity"')
|
||||
assert "这个物品本身是一个小容器" in response.text
|
||||
assert "保存并添加下一个" in response.text
|
||||
|
||||
@@ -893,6 +941,7 @@ def test_new_subitem_page_shows_clear_context_and_default_quantity(client, db_se
|
||||
assert "文件袋" in response.text
|
||||
assert 'name="quantity"' in response.text
|
||||
assert 'value="1"' in response.text
|
||||
assert response.text.index('name="name"') < response.text.index('name="image_file"') < response.text.index('name="quantity"')
|
||||
assert "保存并添加下一个" in response.text
|
||||
|
||||
|
||||
@@ -926,6 +975,7 @@ def test_item_detail_page_renders_clear_hierarchy(client, db_session):
|
||||
assert "书房箱" in response.text
|
||||
assert "SubItem" in response.text
|
||||
assert f'data-href="/subitems/{subitem.id}/edit"' in response.text
|
||||
assert f'action="/subitems/{subitem.id}/delete"' in response.text
|
||||
assert "overview-grid" in response.text
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user