From 148cfdf2a4891d7d9761e30c3d206105ce02d690 Mon Sep 17 00:00:00 2001 From: Zichao Lin Date: Sun, 5 Jul 2026 23:45:23 +0800 Subject: [PATCH] feat(showcase): add public showcase page with per-profile/per-postcard visibility controls --- app/routers/web.py | 92 ++++++++++++++++----- app/static/style.css | 25 ++++++ app/templates/profiles.html | 1 + app/templates/settings.html | 7 ++ app/templates/showcase.html | 123 +++++++++++++++++++++++++++++ app/templates/showcase_manage.html | 78 ++++++++++++++++++ 6 files changed, 307 insertions(+), 19 deletions(-) create mode 100644 app/templates/showcase.html create mode 100644 app/templates/showcase_manage.html diff --git a/app/routers/web.py b/app/routers/web.py index d5ab954..c464d4f 100644 --- a/app/routers/web.py +++ b/app/routers/web.py @@ -456,26 +456,80 @@ async def upload_image( return _redirect(f"/postcards/{postcard_id}") -# ---------- API Keys ---------- +# ---------- Showcase ---------- -@router.get("/api-keys", response_class=HTMLResponse) -def api_keys_page(request: Request, user: User = Depends(get_current_user_web), db: Session = Depends(get_db)): - keys = db.query(ApiKey).filter(ApiKey.user_id == user.id).all() - return templates.TemplateResponse("api_keys.html", {"request": request, "user": user, "keys": keys}) +@router.get("/showcase/manage", response_class=HTMLResponse) +def showcase_manage(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() + profile_cards = {} + for p in profiles: + cards = db.query(Postcard).filter(Postcard.profile_id == p.id).all() + profile_cards[p.id] = cards + return templates.TemplateResponse("showcase_manage.html", { + "request": request, "user": user, "profiles": profiles, "profile_cards": profile_cards, + }) -@router.post("/api-keys") -def api_key_create(user: User = Depends(get_current_user_web), db: Session = Depends(get_db)): - ak = ApiKey(user_id=user.id, key=ApiKey.generate_key()) - db.add(ak) - db.commit() - return _redirect("/api-keys") - - -@router.post("/api-keys/{key_id}/delete") -def api_key_delete(key_id: int, user: User = Depends(get_current_user_web), db: Session = Depends(get_db)): - ak = db.query(ApiKey).filter(ApiKey.id == key_id, ApiKey.user_id == user.id).first() - if ak: - db.delete(ak) +@router.post("/showcase/manage") +def showcase_manage_save(request: Request, showcase_mode: str = Form("profile"), user: User = Depends(get_current_user_web), db: Session = Depends(get_db)): + if showcase_mode in ("profile", "flat"): + user.showcase_mode = showcase_mode db.commit() - return _redirect("/api-keys") + return _redirect("/showcase/manage") + + +@router.post("/profiles/{profile_id}/showcase") +def profile_showcase_save( + profile_id: int, + showcase_enabled: str = Form(""), + showcase_visible: str = Form("both"), + user: User = Depends(get_current_user_web), + db: Session = Depends(get_db), +): + p = db.query(Profile).filter(Profile.id == profile_id, Profile.user_id == user.id).first() + if p: + p.showcase_enabled = showcase_enabled == "1" + if showcase_visible in ("sent", "received", "both"): + p.showcase_visible = showcase_visible + db.commit() + return _redirect("/showcase/manage") + + +@router.post("/postcards/{postcard_id}/showcase-toggle") +def postcard_showcase_toggle(postcard_id: int, showcase_hidden: str = Form(""), user: User = Depends(get_current_user_web), db: Session = Depends(get_db)): + pc = db.query(Postcard).filter(Postcard.id == postcard_id).first() + if pc: + profile = db.query(Profile).filter(Profile.id == pc.profile_id, Profile.user_id == user.id).first() + if profile and profile.showcase_enabled: + pc.showcase_hidden = showcase_hidden == "1" + db.commit() + return _redirect("/showcase/manage") + + +@router.get("/u/{username}", response_class=HTMLResponse) +def public_showcase(request: Request, username: str, db: Session = Depends(get_db)): + user = db.query(User).filter(User.username == username).first() + if not user: + return HTMLResponse("用户不存在", status_code=404) + profiles = db.query(Profile).filter(Profile.user_id == user.id, Profile.showcase_enabled == True).all() + sent_all, received_all, profile_groups = [], [], [] + for p in profiles: + cards = db.query(Postcard).filter(Postcard.profile_id == p.id, Postcard.showcase_hidden == False).all() + if p.showcase_visible in ("sent", "both"): + sent_all.extend([c for c in cards if c.status in ("sent", "delivered")]) + if p.showcase_visible in ("received", "both"): + received_all.extend([c for c in cards if c.status == "received"]) + if p.showcase_visible in ("sent", "both"): + p_sent = [c for c in cards if c.status in ("sent", "delivered")] + else: + p_sent = [] + if p.showcase_visible in ("received", "both"): + p_received = [c for c in cards if c.status == "received"] + else: + p_received = [] + if p_sent or p_received: + profile_groups.append({"profile": p, "sent": p_sent, "received": p_received}) + return templates.TemplateResponse("showcase.html", { + "request": request, "profile_user": user, "mode": user.showcase_mode, + "profile_groups": profile_groups, "sent_all": sent_all, "received_all": received_all, + }) diff --git a/app/static/style.css b/app/static/style.css index 82a0180..9b7f155 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -226,6 +226,27 @@ code { background: var(--bg); padding: .15rem .4rem; border-radius: 4px; font-si /* Empty state */ .empty { color: var(--text-muted); padding: 2rem; text-align: center; } +/* Toggle switch */ +.toggle-switch { position: relative; display: inline-block; width: 36px; height: 20px; } +.toggle-switch input { opacity: 0; width: 0; height: 0; } +.toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: var(--border); border-radius: 20px; transition: .2s; } +.toggle-slider::before { content: ""; position: absolute; height: 16px; width: 16px; left: 2px; bottom: 2px; background: #fff; border-radius: 50%; transition: .2s; } +.toggle-switch input:checked + .toggle-slider { background: var(--primary); } +.toggle-switch input:checked + .toggle-slider::before { transform: translateX(16px); } +.toggle-sm { width: 30px; height: 18px; } +.toggle-sm .toggle-slider::before { height: 14px; width: 14px; } +.toggle-sm input:checked + .toggle-slider::before { transform: translateX(12px); } + +/* Showcase */ +.showcase-card { display: block; background: var(--card-bg); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; transition: box-shadow .15s; text-decoration: none; color: var(--text); } +.showcase-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,.08); text-decoration: none; } +.showcase-card-img { width: 100%; aspect-ratio: 3/2; object-fit: cover; display: block; background: var(--bg); } +.showcase-card-img.placeholder-img { display: flex; align-items: center; justify-content: center; font-size: 2rem; } +.showcase-card-body { padding: .75rem; } +.showcase-card-body strong { font-size: .9rem; } +.showcase-card-meta { display: flex; gap: .5rem; font-size: .8rem; color: var(--text-muted); margin-top: .25rem; } +.showcase-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: .75rem; margin-bottom: 1rem; } + /* Responsive */ @media (max-width: 640px) { .detail-grid { grid-template-columns: 1fr; } @@ -247,4 +268,8 @@ code { background: var(--bg); padding: .15rem .4rem; border-radius: 4px; font-si .btn-back { padding: .25rem .5rem; font-size: .85rem; } .profile-grid { grid-template-columns: 1fr; } .profile-card-stats { gap: .75rem; flex-wrap: wrap; } + .showcase-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: .5rem; } + .showcase-card-meta { flex-wrap: wrap; } + .detail-title-row h1 { font-size: 1.2rem; } + .btn-back { padding: .25rem .5rem; font-size: .85rem; } } diff --git a/app/templates/profiles.html b/app/templates/profiles.html index 36201ab..4143b45 100644 --- a/app/templates/profiles.html +++ b/app/templates/profiles.html @@ -52,6 +52,7 @@ 创建于 {{ p.created_at.strftime('%Y-%m-%d') }}
查看明信片 + 展示管理
diff --git a/app/templates/settings.html b/app/templates/settings.html index 4c18288..09c2500 100644 --- a/app/templates/settings.html +++ b/app/templates/settings.html @@ -5,6 +5,13 @@

用户设置

+
+
+

展示设置

+ 全局展示设置 +
+
+ {% if success %}
{{ success }}
{% endif %} diff --git a/app/templates/showcase.html b/app/templates/showcase.html new file mode 100644 index 0000000..b39f9fc --- /dev/null +++ b/app/templates/showcase.html @@ -0,0 +1,123 @@ +{% extends "base.html" %} +{% block title %}{{ profile_user.username }} 的展示页 - 明信片管理器{% endblock %} +{% block content %} +
+

📮 {{ profile_user.username }} 的明信片

+
+ +{% set has_content = (sent_all|length + received_all|length) > 0 %} +{% if not has_content %} +
+

暂无展示内容

+
+{% else %} + +
+ + +
+ +
+{% if mode == 'flat' %} +
+ {% for pc in sent_all %} +
+ {% if pc.image_front %} + + {% else %} +
📷
+ {% endif %} +
+ {{ pc.card_number }} +
+ {{ pc.country|flag }} {{ pc.country or '' }} + {% if pc.send_time %}{{ pc.send_time.strftime('%Y-%m-%d') }}{% endif %} +
+
+
+ {% endfor %} +
+{% else %} + {% for g in profile_groups %} + {% if g.sent %} +

{{ g.profile.nickname }}

+
+ {% for pc in g.sent %} +
+ {% if pc.image_front %} + + {% else %} +
📷
+ {% endif %} +
+ {{ pc.card_number }} +
+ {{ pc.country|flag }} {{ pc.country or '' }} + {% if pc.send_time %}{{ pc.send_time.strftime('%Y-%m-%d') }}{% endif %} +
+
+
+ {% endfor %} +
+ {% endif %} + {% endfor %} +{% endif %} +
+ + + + +{% endif %} +{% endblock %} diff --git a/app/templates/showcase_manage.html b/app/templates/showcase_manage.html new file mode 100644 index 0000000..d078dad --- /dev/null +++ b/app/templates/showcase_manage.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} +{% block title %}展示管理 - 明信片管理器{% endblock %} +{% block content %} +
+
+ +

展示管理

+
+
+ +
+

全局展示设置

+
+ + +
+ +
+
+

+ 公开展示页地址:/u/{{ user.username }} +

+
+ +
+

各 Profile 展示设置

+ {% if profiles %} + {% for p in profiles %} + {% set pc_list = profile_cards.get(p.id, []) %} +
+
+ {{ p.nickname }} + {% if p.showcase_enabled %}展示中{% else %}未开启{% endif %} +
+
+
+ + +
+
+ + +
+
+ {% if p.showcase_enabled and pc_list %} +
+

单张明信片展示控制(关闭展示后可单独隐藏):

+ {% for pc in pc_list %} +
+ {{ pc.card_number }} + + {% if pc.showcase_hidden %}隐藏{% endif %} +
+ {% endfor %} +
+ {% endif %} +
+ {% endfor %} + {% else %} +
+

暂无 Profile,请先创建

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