数据同步 Tab 改为按品种分组折叠卡片,支持产品级同步,显示数据条数
This commit is contained in:
+35
-12
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, Form, Request
|
|||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.database import get_db
|
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
|
from app.collector import sync_active_contracts, sync_one_contract
|
||||||
|
|
||||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||||
@@ -22,23 +22,35 @@ def admin_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
.order_by(Contract.code)
|
.order_by(Contract.code)
|
||||||
.all()
|
.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({
|
product_data.append({
|
||||||
"id": p.id,
|
"id": p.id,
|
||||||
"code": p.code,
|
"code": p.code,
|
||||||
"name": p.name,
|
"name": p.name,
|
||||||
"exchange": p.exchange,
|
"exchange": p.exchange,
|
||||||
"contracts": [
|
"contracts": contract_list,
|
||||||
{"id": c.id, "code": c.code, "name": c.name, "is_active": c.is_active}
|
|
||||||
for c in contracts
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
total_contracts = sum(len(pd["contracts"]) for pd in product_data)
|
||||||
|
|
||||||
template = request.app.state.templates.get_template("admin.html")
|
template = request.app.state.templates.get_template("admin.html")
|
||||||
return HTMLResponse(
|
return HTMLResponse(
|
||||||
template.render(
|
template.render(
|
||||||
request=request,
|
request=request,
|
||||||
active_nav="admin",
|
active_nav="admin",
|
||||||
products=product_data,
|
products=product_data,
|
||||||
|
total_contracts=total_contracts,
|
||||||
exchanges=EXCHANGES,
|
exchanges=EXCHANGES,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -57,7 +69,7 @@ def create_product(
|
|||||||
p = Product(code=code.upper(), name=name, exchange=exchange)
|
p = Product(code=code.upper(), name=name, exchange=exchange)
|
||||||
db.add(p)
|
db.add(p)
|
||||||
db.commit()
|
db.commit()
|
||||||
return RedirectResponse("/admin/", status_code=303)
|
return RedirectResponse("/admin/?tab=product", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/contract")
|
@router.post("/contract")
|
||||||
@@ -78,7 +90,7 @@ def create_contract(
|
|||||||
)
|
)
|
||||||
db.add(c)
|
db.add(c)
|
||||||
db.commit()
|
db.commit()
|
||||||
return RedirectResponse("/admin/", status_code=303)
|
return RedirectResponse("/admin/?tab=contract", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/contract/{contract_id}/toggle")
|
@router.post("/contract/{contract_id}/toggle")
|
||||||
@@ -87,7 +99,7 @@ def toggle_contract(contract_id: int, db: Session = Depends(get_db)):
|
|||||||
if c:
|
if c:
|
||||||
c.is_active = not c.is_active
|
c.is_active = not c.is_active
|
||||||
db.commit()
|
db.commit()
|
||||||
return RedirectResponse("/admin/", status_code=303)
|
return RedirectResponse("/admin/?tab=contract", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/contract/{contract_id}/delete")
|
@router.post("/contract/{contract_id}/delete")
|
||||||
@@ -96,7 +108,7 @@ def delete_contract(contract_id: int, db: Session = Depends(get_db)):
|
|||||||
if c:
|
if c:
|
||||||
db.delete(c)
|
db.delete(c)
|
||||||
db.commit()
|
db.commit()
|
||||||
return RedirectResponse("/admin/", status_code=303)
|
return RedirectResponse("/admin/?tab=contract", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/product/{product_id}/delete")
|
@router.post("/product/{product_id}/delete")
|
||||||
@@ -105,7 +117,7 @@ def delete_product(product_id: int, db: Session = Depends(get_db)):
|
|||||||
if p:
|
if p:
|
||||||
db.delete(p)
|
db.delete(p)
|
||||||
db.commit()
|
db.commit()
|
||||||
return RedirectResponse("/admin/", status_code=303)
|
return RedirectResponse("/admin/?tab=product", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/sync")
|
@router.post("/sync")
|
||||||
@@ -113,10 +125,21 @@ def sync_all(request: Request):
|
|||||||
results = sync_active_contracts()
|
results = sync_active_contracts()
|
||||||
total = sum(results.values())
|
total = sum(results.values())
|
||||||
print(f"[sync] Synced {total} bars across {len(results)} contracts: {results}")
|
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}")
|
@router.post("/sync/{contract_code}")
|
||||||
def sync_single(contract_code: str):
|
def sync_single(contract_code: str):
|
||||||
count = sync_one_contract(contract_code.upper())
|
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)
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
|
|
||||||
{# ═══════════════════ Tab: 数据同步 ═══════════════════ #}
|
{# ═══════════════════ Tab: 数据同步 ═══════════════════ #}
|
||||||
<div class="tab-panel{% if tab == 'sync' %} active{% endif %}" id="tab-sync">
|
<div class="tab-panel{% if tab == 'sync' %} active{% endif %}" id="tab-sync">
|
||||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;padding:14px 18px;background:var(--surface);border:1px solid var(--border);border-radius:10px;">
|
{# ── Global sync ── #}
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px;padding:14px 18px;background:var(--surface);border:1px solid var(--border);border-radius:10px;">
|
||||||
<span style="font-size:0.85rem;color:var(--sub);">全部合约</span>
|
<span style="font-size:0.85rem;color:var(--sub);">全部合约</span>
|
||||||
<form method="post" action="/admin/sync" style="display:inline;">
|
<form method="post" action="/admin/sync" style="display:inline;">
|
||||||
<button type="submit" class="btn btn-primary" style="font-size:0.82rem;padding:6px 16px;">同步全部</button>
|
<button type="submit" class="btn btn-primary" style="font-size:0.82rem;padding:6px 16px;">同步全部</button>
|
||||||
@@ -37,29 +38,48 @@
|
|||||||
{% if request.query_params.get('synced') %}
|
{% if request.query_params.get('synced') %}
|
||||||
<span style="font-size:0.82rem;color:var(--success-fg);">✓ 已同步 {{ request.query_params.synced }} 条</span>
|
<span style="font-size:0.82rem;color:var(--success-fg);">✓ 已同步 {{ request.query_params.synced }} 条</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<span style="font-size:0.78rem;color:var(--sub);margin-left:auto;">
|
||||||
|
{{ total_contracts }} 个合约
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if products %}
|
{% if products %}
|
||||||
<div class="table-wrap">
|
{% for p in products %}
|
||||||
<table>
|
{# ── Product card ── #}
|
||||||
<tr><th>合约</th><th>品种</th><th>状态</th><th>操作</th></tr>
|
<div style="margin-bottom:16px;border:1px solid var(--border);border-radius:10px;overflow:hidden;">
|
||||||
{% for p in products %}
|
<div class="product-header"
|
||||||
{% for c in p.contracts %}
|
style="display:flex;align-items:center;gap:12px;padding:12px 18px;background:var(--th-bg);cursor:pointer;user-select:none;"
|
||||||
<tr>
|
onclick="toggleProduct(this)">
|
||||||
<td><strong>{{ c.code }}</strong></td>
|
<span class="collapse-arrow" style="font-size:0.75rem;transition:transform .2s;display:inline-block;">▼</span>
|
||||||
<td>{{ p.name }}</td>
|
<strong style="font-size:0.92rem;">{{ p.code }} · {{ p.name }}</strong>
|
||||||
<td>{% if c.is_active %}<span class="badge badge-up">启用</span>{% else %}<span class="badge badge-down">停用</span>{% endif %}</td>
|
<span style="font-size:0.78rem;color:var(--sub);">{{ p.exchange }}</span>
|
||||||
<td>
|
<span style="font-size:0.78rem;color:var(--sub);margin-left:8px;">{{ p.contracts|length }} 合约</span>
|
||||||
<a href="/contracts/{{ c.code }}" style="color:var(--accent);text-decoration:none;font-size:0.82rem;">查看</a>
|
<form method="post" action="/admin/sync/product/{{ p.id }}" style="display:inline;margin-left:auto;" onclick="event.stopPropagation()">
|
||||||
<form method="post" action="/admin/sync/{{ c.code }}" style="display:inline;margin-left:8px;">
|
<button type="submit" class="btn" style="font-size:0.78rem;padding:4px 14px;background:var(--accent);color:#fff;border-radius:5px;">同步该品种</button>
|
||||||
<button style="background:none;border:none;color:var(--accent);cursor:pointer;font-size:0.82rem;">↻ 同步</button>
|
</form>
|
||||||
</form>
|
</div>
|
||||||
</td>
|
<div class="product-body" style="display:block;">
|
||||||
</tr>
|
<table style="font-size:0.84rem;">
|
||||||
{% endfor %}
|
<tr><th style="text-align:left;padding:8px 14px;">合约</th><th style="text-align:center;padding:8px 14px;">数据条数</th><th style="text-align:center;padding:8px 14px;">状态</th><th style="text-align:center;padding:8px 14px;">操作</th></tr>
|
||||||
{% endfor %}
|
{% for c in p.contracts %}
|
||||||
</table>
|
<tr>
|
||||||
|
<td style="text-align:left;padding:6px 14px;"><strong>{{ c.code }}</strong></td>
|
||||||
|
<td style="text-align:center;padding:6px 14px;">{{ c.bar_count }}</td>
|
||||||
|
<td style="text-align:center;padding:6px 14px;">
|
||||||
|
{% if c.is_active %}<span class="badge badge-up">启用</span>{% else %}<span class="badge badge-down">停用</span>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td style="text-align:center;padding:6px 14px;">
|
||||||
|
<a href="/contracts/{{ c.code }}" style="color:var(--accent);text-decoration:none;font-size:0.82rem;">查看</a>
|
||||||
|
<form method="post" action="/admin/sync/{{ c.code }}" style="display:inline;margin-left:8px;">
|
||||||
|
<button style="background:none;border:none;color:var(--accent);cursor:pointer;font-size:0.82rem;">↻ 同步</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div style="text-align:center;padding:40px;color:var(--sub);">暂无品种,请先在「品种管理」中新建。</div>
|
<div style="text-align:center;padding:40px;color:var(--sub);">暂无品种,请先在「品种管理」中新建。</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -179,5 +199,17 @@ function switchTab(name) {
|
|||||||
url.searchParams.set('tab', name);
|
url.searchParams.set('tab', name);
|
||||||
history.replaceState(null, '', url);
|
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)';
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user