diff --git a/ft-app/app/routers/admin.py b/ft-app/app/routers/admin.py index 9f5c305..38ca7ff 100644 --- a/ft-app/app/routers/admin.py +++ b/ft-app/app/routers/admin.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, Form, Request from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy.orm import Session from app.database import get_db -from app.models import Product, Contract +from app.models import Product, Contract, DailyBar from app.collector import sync_active_contracts, sync_one_contract router = APIRouter(prefix="/admin", tags=["admin"]) @@ -22,23 +22,35 @@ def admin_page(request: Request, db: Session = Depends(get_db)): .order_by(Contract.code) .all() ) + contract_list = [] + for c in contracts: + bar_count = ( + db.query(DailyBar) + .filter(DailyBar.contract == c.code) + .count() + ) + contract_list.append({ + "id": c.id, "code": c.code, "name": c.name, + "is_active": c.is_active, "bar_count": bar_count, + }) + product_data.append({ "id": p.id, "code": p.code, "name": p.name, "exchange": p.exchange, - "contracts": [ - {"id": c.id, "code": c.code, "name": c.name, "is_active": c.is_active} - for c in contracts - ], + "contracts": contract_list, }) + total_contracts = sum(len(pd["contracts"]) for pd in product_data) + template = request.app.state.templates.get_template("admin.html") return HTMLResponse( template.render( request=request, active_nav="admin", products=product_data, + total_contracts=total_contracts, exchanges=EXCHANGES, ) ) @@ -57,7 +69,7 @@ def create_product( p = Product(code=code.upper(), name=name, exchange=exchange) db.add(p) db.commit() - return RedirectResponse("/admin/", status_code=303) + return RedirectResponse("/admin/?tab=product", status_code=303) @router.post("/contract") @@ -78,7 +90,7 @@ def create_contract( ) db.add(c) db.commit() - return RedirectResponse("/admin/", status_code=303) + return RedirectResponse("/admin/?tab=contract", status_code=303) @router.post("/contract/{contract_id}/toggle") @@ -87,7 +99,7 @@ def toggle_contract(contract_id: int, db: Session = Depends(get_db)): if c: c.is_active = not c.is_active db.commit() - return RedirectResponse("/admin/", status_code=303) + return RedirectResponse("/admin/?tab=contract", status_code=303) @router.post("/contract/{contract_id}/delete") @@ -96,7 +108,7 @@ def delete_contract(contract_id: int, db: Session = Depends(get_db)): if c: db.delete(c) db.commit() - return RedirectResponse("/admin/", status_code=303) + return RedirectResponse("/admin/?tab=contract", status_code=303) @router.post("/product/{product_id}/delete") @@ -105,7 +117,7 @@ def delete_product(product_id: int, db: Session = Depends(get_db)): if p: db.delete(p) db.commit() - return RedirectResponse("/admin/", status_code=303) + return RedirectResponse("/admin/?tab=product", status_code=303) @router.post("/sync") @@ -113,10 +125,21 @@ def sync_all(request: Request): results = sync_active_contracts() total = sum(results.values()) print(f"[sync] Synced {total} bars across {len(results)} contracts: {results}") - return RedirectResponse("/admin/?synced=" + str(total), status_code=303) + return RedirectResponse(f"/admin/?tab=sync&synced={total}", status_code=303) @router.post("/sync/{contract_code}") def sync_single(contract_code: str): count = sync_one_contract(contract_code.upper()) - return RedirectResponse(f"/admin/?synced={count}", status_code=303) + return RedirectResponse(f"/admin/?tab=sync&synced={count}", status_code=303) + + +@router.post("/sync/product/{product_id}") +def sync_product(product_id: int, request: Request, db: Session = Depends(get_db)): + contracts = db.query(Contract).filter( + Contract.product_id == product_id, Contract.is_active == True + ).all() + total = 0 + for c in contracts: + total += sync_one_contract(c.code) + return RedirectResponse(f"/admin/?tab=sync&synced={total}", status_code=303) diff --git a/ft-app/app/templates/admin.html b/ft-app/app/templates/admin.html index ba16f67..7d903a3 100644 --- a/ft-app/app/templates/admin.html +++ b/ft-app/app/templates/admin.html @@ -29,7 +29,8 @@ {# ═══════════════════ Tab: 数据同步 ═══════════════════ #}
-
+ {# ── Global sync ── #} +
全部合约
@@ -37,29 +38,48 @@ {% if request.query_params.get('synced') %} ✓ 已同步 {{ request.query_params.synced }} 条 {% endif %} + + {{ total_contracts }} 个合约 +
{% if products %} -
- - - {% for p in products %} - {% for c in p.contracts %} - - - - - - - {% endfor %} - {% endfor %} -
合约品种状态操作
{{ c.code }}{{ p.name }}{% if c.is_active %}启用{% else %}停用{% endif %} - 查看 - - - -
+ {% for p in products %} + {# ── Product card ── #} +
+
+ + {{ p.code }} · {{ p.name }} + {{ p.exchange }} + {{ p.contracts|length }} 合约 +
+ +
+
+
+ + + {% for c in p.contracts %} + + + + + + + {% endfor %} +
合约数据条数状态操作
{{ c.code }}{{ c.bar_count }} + {% if c.is_active %}启用{% else %}停用{% endif %} + + 查看 +
+ +
+
+
+ {% endfor %} {% else %}
暂无品种,请先在「品种管理」中新建。
{% endif %} @@ -179,5 +199,17 @@ function switchTab(name) { url.searchParams.set('tab', name); history.replaceState(null, '', url); } + +function toggleProduct(header) { + var body = header.nextElementSibling; + var arrow = header.querySelector('.collapse-arrow'); + if (body.style.display === 'none') { + body.style.display = 'block'; + arrow.style.transform = 'rotate(0deg)'; + } else { + body.style.display = 'none'; + arrow.style.transform = 'rotate(-90deg)'; + } +} {% endblock %}