feat(ui): redesign postcards list with tabs, row layout, country dropdown filter

This commit is contained in:
2026-07-05 22:12:33 +08:00
parent 2aeaa560d6
commit 84e4e457db
3 changed files with 99 additions and 35 deletions

View File

@@ -203,7 +203,6 @@ def profile_rename(profile_id: int, nickname: str = Form(...), user: User = Depe
def postcard_list( def postcard_list(
request: Request, request: Request,
profile_id: int, profile_id: int,
status: str = "",
country: str = "", country: str = "",
user: User = Depends(get_current_user_web), user: User = Depends(get_current_user_web),
db: Session = Depends(get_db), db: Session = Depends(get_db),
@@ -212,14 +211,24 @@ def postcard_list(
if not profile: if not profile:
return _redirect("/profiles") return _redirect("/profiles")
q = db.query(Postcard).filter(Postcard.profile_id == profile_id) q = db.query(Postcard).filter(Postcard.profile_id == profile_id)
if status:
q = q.filter(Postcard.status == status)
if country: if country:
q = q.filter(Postcard.country.ilike(f"%{country}%")) q = q.filter(Postcard.country == country)
postcards = q.order_by(Postcard.created_at.desc()).all() postcards = q.all()
# collect existing countries for dropdown
all_countries = sorted({pc.country for pc in db.query(Postcard.country).filter(Postcard.profile_id == profile_id, Postcard.country.isnot(None)).all()})
# status sort order: sent=0, delivered=1, received=2
_status_order = {"sent": 0, "delivered": 1, "received": 2}
postcards.sort(key=lambda pc: (_status_order.get(pc.status, 9), -(pc.send_time or pc.arrival_time or pc.receive_time or pc.created_at).timestamp()))
# split into sent and received
sent = [pc for pc in postcards if pc.status in ("sent", "delivered")]
received = [pc for pc in postcards if pc.status == "received"]
return templates.TemplateResponse( return templates.TemplateResponse(
"postcards.html", "postcards.html",
{"request": request, "user": user, "profile": profile, "postcards": postcards, "status": status, "country": country}, {"request": request, "user": user, "profile": profile, "sent": sent, "received": received, "countries": all_countries, "country": country},
) )

View File

@@ -136,6 +136,23 @@ code { background: var(--bg); padding: .15rem .4rem; border-radius: 4px; font-si
.detail-meta dt { font-weight: 600; font-size: .85rem; color: var(--text-muted); } .detail-meta dt { font-weight: 600; font-size: .85rem; color: var(--text-muted); }
.detail-meta dd { font-size: .95rem; } .detail-meta dd { font-size: .95rem; }
/* Tabs */
.tabs { display: flex; gap: 0; border-bottom: 2px solid var(--border); margin-bottom: 1rem; }
.tab { padding: .5rem 1.25rem; border: none; background: none; cursor: pointer; font-size: .9rem; color: var(--text-muted); border-bottom: 2px solid transparent; margin-bottom: -2px; }
.tab.active { color: var(--primary); border-bottom-color: var(--primary); font-weight: 600; }
.tab:hover { color: var(--primary); }
/* Postcard row */
.postcard-row { display: flex; align-items: center; gap: 1rem; padding: .75rem; border: 1px solid var(--border); border-radius: 8px; background: var(--card-bg); margin-bottom: .5rem; text-decoration: none; color: var(--text); transition: box-shadow .15s; }
.postcard-row:hover { box-shadow: 0 2px 8px rgba(0,0,0,.08); text-decoration: none; }
.postcard-row-img { flex-shrink: 0; width: 56px; height: 56px; border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }
.postcard-row-img img { width: 100%; height: 100%; object-fit: cover; }
.postcard-row-img .placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: var(--bg); font-size: 1.2rem; }
.postcard-row-info { flex: 1; display: flex; align-items: center; gap: .75rem; min-width: 0; font-size: .9rem; }
.postcard-row-info strong { white-space: nowrap; }
.postcard-row-info span { white-space: nowrap; color: var(--text-muted); }
.postcard-row-action { flex-shrink: 0; }
/* Filter */ /* Filter */
.filter-bar { margin-bottom: 1.25rem; } .filter-bar { margin-bottom: 1.25rem; }
@@ -146,4 +163,5 @@ code { background: var(--bg); padding: .15rem .4rem; border-radius: 4px; font-si
@media (max-width: 640px) { @media (max-width: 640px) {
.detail-grid { grid-template-columns: 1fr; } .detail-grid { grid-template-columns: 1fr; }
.card-grid { grid-template-columns: 1fr; } .card-grid { grid-template-columns: 1fr; }
.postcard-row-info { flex-wrap: wrap; gap: .35rem; }
} }

View File

@@ -7,43 +7,80 @@
</div> </div>
<div class="filter-bar"> <div class="filter-bar">
<form method="get" class="form-row"> <form method="get" class="form-row">
<select name="status"> <select name="country">
<option value="">全部状态</option> <option value="">全部国家</option>
<option value="sent" {% if status == 'sent' %}selected{% endif %}>已寄出</option> {% for c in countries %}
<option value="delivered" {% if status == 'delivered' %}selected{% endif %}>已送达</option> <option value="{{ c }}" {% if country == c %}selected{% endif %}>{{ c|flag }} {{ c }}</option>
<option value="received" {% if status == 'received' %}selected{% endif %}>已收到</option> {% endfor %}
</select> </select>
<input type="text" name="country" placeholder="国家/地区" value="{{ country }}">
<button type="submit" class="btn">筛选</button> <button type="submit" class="btn">筛选</button>
</form> </form>
</div> </div>
{% if postcards %}
<div class="card-grid"> <div class="tabs" id="tabs">
{% for pc in postcards %} <button class="tab active" onclick="switchTab('sent')">寄出 ({{ sent|length }})</button>
<a href="/postcards/{{ pc.id }}" class="postcard-card"> <button class="tab" onclick="switchTab('received')">收到 ({{ received|length }})</button>
</div>
<div id="tab-sent" class="tab-panel">
{% if sent %}
{% for pc in sent %}
<a href="/postcards/{{ pc.id }}" class="postcard-row">
<div class="postcard-row-img">
{% if pc.image_front %} {% if pc.image_front %}
<img src="{{ pc.image_front }}" alt="正面" class="thumb"> <img src="{{ pc.image_front }}" alt="正面">
{% else %} {% else %}
<div class="thumb placeholder">📷</div> <div class="thumb placeholder">📷</div>
{% endif %} {% endif %}
<div class="postcard-info"> </div>
<span class="badge badge-{{ pc.status }}">{{ {'sent':'已寄出','delivered':'已送达','received':'已收到'}.get(pc.status, pc.status) }}</span> <div class="postcard-row-info">
<strong>{{ pc.card_number }}</strong> <strong>{{ pc.card_number }}</strong>
{% if pc.status in ('sent','delivered') %} <span>{{ pc.country|flag }} {{ pc.country or '-' }}</span>
<span>→ {{ pc.recipient_name or '-' }} ({{ pc.country|flag }} {{ pc.country or '-' }})</span> <span>→ {{ pc.recipient_name or '-' }}</span>
{% else %} <span class="badge badge-{{ pc.status }}">{{ {'sent':'已寄出','delivered':'已送达'}.get(pc.status) }}</span>
<span>← {{ pc.sender_name or '-' }}</span> </div>
{% endif %} <div class="postcard-row-action">
{% if pc.status == 'sent' %} {% if pc.status == 'sent' %}
<form method="post" action="/postcards/{{ pc.id }}/mark-delivered" class="inline" onclick="event.stopPropagation();"> <form method="post" action="/postcards/{{ pc.id }}/mark-delivered" onclick="event.preventDefault();event.stopPropagation();if(confirm('确认已送达?'))this.submit();">
<button type="submit" class="btn btn-sm btn-primary" onclick="return confirm('确认已送达?')">✓ 标记已送达</button> <button type="submit" class="btn btn-sm btn-primary">✓ 标记已送达</button>
</form> </form>
{% endif %} {% endif %}
</div> </div>
</a> </a>
{% endfor %} {% endfor %}
</div>
{% else %} {% else %}
<p class="empty">没有找到明信片。</p> <p class="empty">没有寄出的明信片。</p>
{% endif %} {% endif %}
</div>
<div id="tab-received" class="tab-panel" style="display:none">
{% if received %}
{% for pc in received %}
<a href="/postcards/{{ pc.id }}" class="postcard-row">
<div class="postcard-row-img">
{% if pc.image_front %}
<img src="{{ pc.image_front }}" alt="正面">
{% else %}
<div class="thumb placeholder">📷</div>
{% endif %}
</div>
<div class="postcard-row-info">
<strong>{{ pc.card_number }}</strong>
<span>← {{ pc.sender_name or '-' }}</span>
</div>
</a>
{% endfor %}
{% else %}
<p class="empty">没有收到的明信片。</p>
{% endif %}
</div>
<script>
function switchTab(name) {
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
document.querySelectorAll('.tab-panel').forEach(function(p) { p.style.display = 'none'; });
event.currentTarget.classList.add('active');
document.getElementById('tab-' + name).style.display = '';
}
</script>
{% endblock %} {% endblock %}