From a4714db073d0fd12c27a0c4cc2d421161a87fda8 Mon Sep 17 00:00:00 2001 From: Zichao Lin Date: Sun, 5 Jul 2026 22:32:16 +0800 Subject: [PATCH] feat(ui): redesign dashboard as global overview with stats, progress bar, recent activity --- app/routers/web.py | 41 ++++++--- app/static/style.css | 37 ++++++++ app/templates/dashboard.html | 164 ++++++++++++++++++++++++----------- 3 files changed, 179 insertions(+), 63 deletions(-) diff --git a/app/routers/web.py b/app/routers/web.py index 6f9861e..e2ca411 100644 --- a/app/routers/web.py +++ b/app/routers/web.py @@ -148,20 +148,35 @@ def root(user: User = Depends(get_current_user_web)): @router.get("/dashboard", response_class=HTMLResponse) def dashboard(request: Request, user: User = Depends(get_current_user_web), db: Session = Depends(get_db)): profiles = db.query(Profile).filter(Profile.user_id == user.id).all() - stats = {} - extras = {} + all_cards = [] for p in profiles: - cards = db.query(Postcard).filter(Postcard.profile_id == p.id).order_by(Postcard.created_at.desc()).all() - stats[p.id] = { - "total": len(cards), - "sent": sum(1 for c in cards if c.status == "sent"), - "delivered": sum(1 for c in cards if c.status == "delivered"), - "received": sum(1 for c in cards if c.status == "received"), - } - countries = sorted({c.country for c in cards if c.country}) - recent = cards[:3] - extras[p.id] = {"countries": countries, "recent": recent} - return templates.TemplateResponse("dashboard.html", {"request": request, "user": user, "profiles": profiles, "stats": stats, "extras": extras}) + cards = db.query(Postcard).filter(Postcard.profile_id == p.id).all() + all_cards.extend(cards) + + total = len(all_cards) + sent = sum(1 for c in all_cards if c.status == "sent") + delivered = sum(1 for c in all_cards if c.status == "delivered") + received = sum(1 for c in all_cards if c.status == "received") + countries = len({c.country for c in all_cards if c.country}) + + # recent postcards across all profiles (last 8) + all_cards.sort(key=lambda c: c.created_at, reverse=True) + recent = all_cards[:8] + + # status distribution for progress bar + if total > 0: + sent_pct = round(sent / total * 100) + delivered_pct = round(delivered / total * 100) + received_pct = round(received / total * 100) + else: + sent_pct = delivered_pct = received_pct = 0 + + return templates.TemplateResponse("dashboard.html", { + "request": request, "user": user, "profiles": profiles, + "total": total, "sent": sent, "delivered": delivered, "received": received, + "countries": countries, "recent": recent, + "sent_pct": sent_pct, "delivered_pct": delivered_pct, "received_pct": received_pct, + }) # ---------- Profiles ---------- diff --git a/app/static/style.css b/app/static/style.css index 7db0970..3569f72 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -142,6 +142,43 @@ code { background: var(--bg); padding: .15rem .4rem; border-radius: 4px; font-si .tab.active { color: var(--primary); border-bottom-color: var(--primary); font-weight: 600; } .tab:hover { color: var(--primary); } +/* Dashboard */ +.dash-stats { display: flex; gap: .75rem; margin-bottom: 1.5rem; flex-wrap: wrap; } +.dash-stat-card { display: flex; align-items: center; gap: .75rem; background: var(--card-bg); border: 1px solid var(--border); border-radius: 10px; padding: 1rem 1.25rem; flex: 1; min-width: 140px; } +.dash-stat-icon { font-size: 1.5rem; } +.dash-stat-body { display: flex; flex-direction: column; } +.dash-stat-value { font-size: 1.4rem; font-weight: 700; line-height: 1.2; } +.dash-stat-label { font-size: .75rem; color: var(--text-muted); } +.dash-sent { color: var(--sent); } +.dash-delivered { color: var(--delivered); } +.dash-received { color: var(--received); } +.dash-progress { margin-bottom: 2rem; } +.progress-bar { display: flex; height: 8px; border-radius: 4px; overflow: hidden; background: var(--border); } +.progress-seg { height: 100%; } +.progress-sent { background: var(--sent); } +.progress-delivered { background: var(--delivered); } +.progress-received { background: var(--received); } +.progress-legend { display: flex; gap: 1.25rem; margin-top: .5rem; font-size: .8rem; color: var(--text-muted); } +.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: .25rem; vertical-align: middle; } +.dot-sent { background: var(--sent); } +.dot-delivered { background: var(--delivered); } +.dot-received { background: var(--received); } +.dash-sections { display: grid; grid-template-columns: 1.5fr 1fr; gap: 1.5rem; } +.dash-section h2 { font-size: 1rem; margin-bottom: .75rem; } +.recent-list { display: flex; flex-direction: column; gap: .4rem; } +.recent-item { display: flex; align-items: center; gap: .75rem; padding: .6rem .75rem; border: 1px solid var(--border); border-radius: 8px; background: var(--card-bg); text-decoration: none; color: var(--text); transition: box-shadow .15s; } +.recent-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,.06); text-decoration: none; } +.recent-img { width: 40px; height: 40px; border-radius: 6px; overflow: hidden; border: 1px solid var(--border); flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: var(--bg); } +.recent-img img { width: 100%; height: 100%; object-fit: cover; } +.recent-info { display: flex; align-items: center; gap: .5rem; } +.recent-meta { margin-left: auto; display: flex; gap: .5rem; font-size: .8rem; color: var(--text-muted); white-space: nowrap; } +.dash-profiles { display: flex; flex-direction: column; gap: .3rem; } +.dash-profile-link { display: flex; align-items: center; justify-content: space-between; padding: .6rem .75rem; border: 1px solid var(--border); border-radius: 8px; background: var(--card-bg); text-decoration: none; color: var(--text); transition: box-shadow .15s; } +.dash-profile-link:hover { box-shadow: 0 2px 8px rgba(0,0,0,.06); text-decoration: none; } +.dash-profile-name { font-weight: 500; } +.dash-profile-arrow { color: var(--text-muted); } +.dash-profile-more { color: var(--primary); border-style: dashed; } + /* Profile cards */ .profile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; } .profile-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 10px; padding: 1.25rem; transition: box-shadow .15s; } diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index bf371e7..5a58821 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -1,56 +1,120 @@ {% extends "base.html" %} {% block title %}Dashboard - Postcard Manager{% endblock %} {% block content %} -

👋 欢迎,{{ user.username }}

+
+

👋 欢迎,{{ user.username }}

+
-
- {% if profiles %} -
+
+
📤
+
+ {{ sent }} + 已寄出 +
+
+
+
📬
+
+ {{ delivered }} + 已送达 +
+
+
+
💌
+
+ {{ received }} + 已收到 +
+
+
+
🌍
+
+ {{ countries }} + 国家/地区 +
+
+ + +{% if total > 0 %} +
+
+
+
+
+
+
+ 已寄出 {{ sent_pct }}% + 已送达 {{ delivered_pct }}% + 已收到 {{ received_pct }}% +
+
+{% endif %} + +
+ + +
+

Profiles

+
+ {% for p in profiles %} + + {{ p.nickname }} + + + {% endfor %} + + 管理全部 + + +
+
+
+ +{% else %} +
+
📮
+

欢迎使用明信片管理器!

+

创建一个 Profile 开始管理你的明信片

+
+{% endif %} {% endblock %}