feat(showcase): lightbox, checkbox per-card toggle, lazyload, fix toggle Y-align, remove navbar on public page
This commit is contained in:
@@ -514,12 +514,12 @@ def profile_showcase_save(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/postcards/{postcard_id}/showcase-toggle")
|
@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)):
|
def postcard_showcase_toggle(postcard_id: int, showcase_visible: str = Form(""), user: User = Depends(get_current_user_web), db: Session = Depends(get_db)):
|
||||||
pc = db.query(Postcard).filter(Postcard.id == postcard_id).first()
|
pc = db.query(Postcard).filter(Postcard.id == postcard_id).first()
|
||||||
if pc:
|
if pc:
|
||||||
profile = db.query(Profile).filter(Profile.id == pc.profile_id, Profile.user_id == user.id).first()
|
profile = db.query(Profile).filter(Profile.id == pc.profile_id, Profile.user_id == user.id).first()
|
||||||
if profile and profile.showcase_enabled:
|
if profile and profile.showcase_enabled:
|
||||||
pc.showcase_hidden = showcase_hidden == "1"
|
pc.showcase_hidden = showcase_visible != "1"
|
||||||
db.commit()
|
db.commit()
|
||||||
return _redirect("/settings?tab=showcase")
|
return _redirect("/settings?tab=showcase")
|
||||||
|
|
||||||
|
|||||||
@@ -227,13 +227,13 @@ code { background: var(--bg); padding: .15rem .4rem; border-radius: 4px; font-si
|
|||||||
.empty { color: var(--text-muted); padding: 2rem; text-align: center; }
|
.empty { color: var(--text-muted); padding: 2rem; text-align: center; }
|
||||||
|
|
||||||
/* Toggle switch */
|
/* Toggle switch */
|
||||||
.toggle-switch { position: relative; display: inline-block; width: 36px; height: 20px; }
|
.toggle-switch { position: relative; display: inline-block; width: 36px; height: 20px; margin: 0; vertical-align: middle; }
|
||||||
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
.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 { 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-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 { background: var(--primary); }
|
||||||
.toggle-switch input:checked + .toggle-slider::before { transform: translateX(16px); }
|
.toggle-switch input:checked + .toggle-slider::before { transform: translateX(16px); }
|
||||||
.toggle-sm { width: 30px; height: 18px; }
|
.toggle-sm { width: 30px; height: 18px; margin: 0; }
|
||||||
.toggle-sm .toggle-slider::before { height: 14px; width: 14px; }
|
.toggle-sm .toggle-slider::before { height: 14px; width: 14px; }
|
||||||
.toggle-sm input:checked + .toggle-slider::before { transform: translateX(12px); }
|
.toggle-sm input:checked + .toggle-slider::before { transform: translateX(12px); }
|
||||||
|
|
||||||
@@ -247,6 +247,18 @@ code { background: var(--bg); padding: .15rem .4rem; border-radius: 4px; font-si
|
|||||||
.showcase-card-meta { display: flex; gap: .5rem; font-size: .8rem; color: var(--text-muted); margin-top: .25rem; }
|
.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; }
|
.showcase-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: .75rem; margin-bottom: 1rem; }
|
||||||
|
|
||||||
|
/* Lightbox */
|
||||||
|
.lightbox { display: none; position: fixed; inset: 0; z-index: 100; background: rgba(0,0,0,.85); justify-content: center; align-items: center; }
|
||||||
|
.lightbox.active { display: flex; }
|
||||||
|
.lightbox-content { position: relative; max-width: 90vw; max-height: 85vh; text-align: center; }
|
||||||
|
.lightbox-content img { max-width: 90vw; max-height: 80vh; object-fit: contain; border-radius: 6px; }
|
||||||
|
.lightbox-caption { color: #fff; font-size: .85rem; margin-top: .5rem; text-align: center; }
|
||||||
|
.lightbox-close { position: absolute; top: .75rem; right: 1rem; background: none; border: none; color: #fff; font-size: 2rem; cursor: pointer; z-index: 101; line-height: 1; }
|
||||||
|
.lightbox-prev, .lightbox-next { position: absolute; top: 50%; transform: translateY(-50%); background: rgba(255,255,255,.15); border: none; color: #fff; font-size: 1.8rem; cursor: pointer; padding: .5rem .75rem; border-radius: 4px; z-index: 101; transition: background .2s; }
|
||||||
|
.lightbox-prev:hover, .lightbox-next:hover { background: rgba(255,255,255,.3); }
|
||||||
|
.lightbox-prev { left: 1rem; }
|
||||||
|
.lightbox-next { right: 1rem; }
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.detail-grid { grid-template-columns: 1fr; }
|
.detail-grid { grid-template-columns: 1fr; }
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% block nav %}
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
{% block navbar %}
|
|
||||||
<a href="/dashboard" class="nav-brand">📮 明信片管理器</a>
|
<a href="/dashboard" class="nav-brand">📮 明信片管理器</a>
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
{% if user is defined and user %}
|
{% if user is defined and user %}
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
<a href="/logout" class="nav-logout">退出</a>
|
<a href="/logout" class="nav-logout">退出</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
</nav>
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
<main class="container">
|
<main class="container">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
<a href="/postcards/{{ pc.id }}" class="recent-item">
|
<a href="/postcards/{{ pc.id }}" class="recent-item">
|
||||||
<div class="recent-img">
|
<div class="recent-img">
|
||||||
{% if pc.image_front %}
|
{% if pc.image_front %}
|
||||||
<img src="{{ pc.image_front }}" alt="">
|
<img src="{{ pc.image_front }}" alt="" loading="lazy">
|
||||||
{% else %}
|
{% else %}
|
||||||
<span>📷</span>
|
<span>📷</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<a href="/postcards/{{ pc.id }}" class="postcard-row">
|
<a href="/postcards/{{ pc.id }}" class="postcard-row">
|
||||||
<div class="postcard-row-img">
|
<div class="postcard-row-img">
|
||||||
{% if pc.image_front %}
|
{% if pc.image_front %}
|
||||||
<img src="{{ pc.image_front }}" alt="正面">
|
<img src="{{ pc.image_front }}" alt="正面" loading="lazy">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="thumb placeholder">📷</div>
|
<div class="thumb placeholder">📷</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<a href="/postcards/{{ pc.id }}" class="postcard-row">
|
<a href="/postcards/{{ pc.id }}" class="postcard-row">
|
||||||
<div class="postcard-row-img">
|
<div class="postcard-row-img">
|
||||||
{% if pc.image_front %}
|
{% if pc.image_front %}
|
||||||
<img src="{{ pc.image_front }}" alt="正面">
|
<img src="{{ pc.image_front }}" alt="正面" loading="lazy">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="thumb placeholder">📷</div>
|
<div class="thumb placeholder">📷</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -113,11 +113,8 @@
|
|||||||
<span style="font-size:.78rem;color:var(--text-muted);margin-right:.5rem">发出</span>
|
<span style="font-size:.78rem;color:var(--text-muted);margin-right:.5rem">发出</span>
|
||||||
{% for pc in sent_cards %}
|
{% for pc in sent_cards %}
|
||||||
<form method="post" action="/postcards/{{ pc.id }}/showcase-toggle" style="display:inline-flex;align-items:center;gap:.3rem;margin-right:.75rem;margin-bottom:.25rem;font-size:.85rem">
|
<form method="post" action="/postcards/{{ pc.id }}/showcase-toggle" style="display:inline-flex;align-items:center;gap:.3rem;margin-right:.75rem;margin-bottom:.25rem;font-size:.85rem">
|
||||||
|
<input type="checkbox" name="showcase_visible" value="1" {% if not pc.showcase_hidden %}checked{% endif %} onchange="this.form.submit()" style="margin:0">
|
||||||
<span>{{ pc.card_number }}</span>
|
<span>{{ pc.card_number }}</span>
|
||||||
<label class="toggle-switch toggle-sm">
|
|
||||||
<input type="checkbox" name="showcase_hidden" value="1" {% if pc.showcase_hidden %}checked{% endif %} onchange="this.form.submit()">
|
|
||||||
<span class="toggle-slider"></span>
|
|
||||||
</label>
|
|
||||||
</form>
|
</form>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -127,11 +124,8 @@
|
|||||||
<span style="font-size:.78rem;color:var(--text-muted);margin-right:.5rem">收到</span>
|
<span style="font-size:.78rem;color:var(--text-muted);margin-right:.5rem">收到</span>
|
||||||
{% for pc in received_cards %}
|
{% for pc in received_cards %}
|
||||||
<form method="post" action="/postcards/{{ pc.id }}/showcase-toggle" style="display:inline-flex;align-items:center;gap:.3rem;margin-right:.75rem;margin-bottom:.25rem;font-size:.85rem">
|
<form method="post" action="/postcards/{{ pc.id }}/showcase-toggle" style="display:inline-flex;align-items:center;gap:.3rem;margin-right:.75rem;margin-bottom:.25rem;font-size:.85rem">
|
||||||
|
<input type="checkbox" name="showcase_visible" value="1" {% if not pc.showcase_hidden %}checked{% endif %} onchange="this.form.submit()" style="margin:0">
|
||||||
<span>{{ pc.card_number }}</span>
|
<span>{{ pc.card_number }}</span>
|
||||||
<label class="toggle-switch toggle-sm">
|
|
||||||
<input type="checkbox" name="showcase_hidden" value="1" {% if pc.showcase_hidden %}checked{% endif %} onchange="this.form.submit()">
|
|
||||||
<span class="toggle-slider"></span>
|
|
||||||
</label>
|
|
||||||
</form>
|
</form>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{{ profile_user.username }} 的展示页{% endblock %}
|
{% block title %}{{ profile_user.username }} 的明信片{% endblock %}
|
||||||
{% block navbar %}{% endblock %}
|
{% block nav %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h1>📮 {{ profile_user.username }} 的明信片</h1>
|
<h1>📮 {{ profile_user.username }} 的明信片</h1>
|
||||||
@@ -22,9 +22,9 @@
|
|||||||
{% if mode == 'flat' %}
|
{% if mode == 'flat' %}
|
||||||
<div class="showcase-grid">
|
<div class="showcase-grid">
|
||||||
{% for pc in sent_all %}
|
{% for pc in sent_all %}
|
||||||
<div class="showcase-card">
|
<div class="showcase-card" data-front="{{ pc.image_front or '' }}" data-number="{{ pc.card_number }}" onclick="openLightbox(this)">
|
||||||
{% if pc.image_front %}
|
{% if pc.image_front %}
|
||||||
<img src="{{ pc.image_front }}" alt="" class="showcase-card-img">
|
<img src="{{ pc.image_front }}" alt="" class="showcase-card-img" loading="lazy">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="showcase-card-img placeholder-img">📷</div>
|
<div class="showcase-card-img placeholder-img">📷</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -44,9 +44,9 @@
|
|||||||
<h3 style="margin:1rem 0 .75rem;font-size:.95rem;color:var(--text-muted)">{{ g.profile.nickname }}</h3>
|
<h3 style="margin:1rem 0 .75rem;font-size:.95rem;color:var(--text-muted)">{{ g.profile.nickname }}</h3>
|
||||||
<div class="showcase-grid">
|
<div class="showcase-grid">
|
||||||
{% for pc in g.sent %}
|
{% for pc in g.sent %}
|
||||||
<div class="showcase-card">
|
<div class="showcase-card" data-front="{{ pc.image_front or '' }}" data-number="{{ pc.card_number }}" onclick="openLightbox(this)">
|
||||||
{% if pc.image_front %}
|
{% if pc.image_front %}
|
||||||
<img src="{{ pc.image_front }}" alt="" class="showcase-card-img">
|
<img src="{{ pc.image_front }}" alt="" class="showcase-card-img" loading="lazy">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="showcase-card-img placeholder-img">📷</div>
|
<div class="showcase-card-img placeholder-img">📷</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -69,9 +69,9 @@
|
|||||||
{% if mode == 'flat' %}
|
{% if mode == 'flat' %}
|
||||||
<div class="showcase-grid">
|
<div class="showcase-grid">
|
||||||
{% for pc in received_all %}
|
{% for pc in received_all %}
|
||||||
<div class="showcase-card">
|
<div class="showcase-card" data-front="{{ pc.image_front or '' }}" data-number="{{ pc.card_number }}" onclick="openLightbox(this)">
|
||||||
{% if pc.image_front %}
|
{% if pc.image_front %}
|
||||||
<img src="{{ pc.image_front }}" alt="" class="showcase-card-img">
|
<img src="{{ pc.image_front }}" alt="" class="showcase-card-img" loading="lazy">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="showcase-card-img placeholder-img">📷</div>
|
<div class="showcase-card-img placeholder-img">📷</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -91,9 +91,9 @@
|
|||||||
<h3 style="margin:1rem 0 .75rem;font-size:.95rem;color:var(--text-muted)">{{ g.profile.nickname }}</h3>
|
<h3 style="margin:1rem 0 .75rem;font-size:.95rem;color:var(--text-muted)">{{ g.profile.nickname }}</h3>
|
||||||
<div class="showcase-grid">
|
<div class="showcase-grid">
|
||||||
{% for pc in g.received %}
|
{% for pc in g.received %}
|
||||||
<div class="showcase-card">
|
<div class="showcase-card" data-front="{{ pc.image_front or '' }}" data-number="{{ pc.card_number }}" onclick="openLightbox(this)">
|
||||||
{% if pc.image_front %}
|
{% if pc.image_front %}
|
||||||
<img src="{{ pc.image_front }}" alt="" class="showcase-card-img">
|
<img src="{{ pc.image_front }}" alt="" class="showcase-card-img" loading="lazy">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="showcase-card-img placeholder-img">📷</div>
|
<div class="showcase-card-img placeholder-img">📷</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -111,6 +111,17 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="lightbox" class="lightbox" onclick="closeLightbox(event)">
|
||||||
|
<button class="lightbox-close" onclick="closeLightbox(event)">×</button>
|
||||||
|
<button class="lightbox-prev" onclick="navLightbox(-1,event)">❮</button>
|
||||||
|
<button class="lightbox-next" onclick="navLightbox(1,event)">❯</button>
|
||||||
|
<div class="lightbox-content" onclick="event.stopPropagation()">
|
||||||
|
<img id="lightbox-img" src="" alt="">
|
||||||
|
<div id="lightbox-caption" class="lightbox-caption"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function switchTab(name) {
|
function switchTab(name) {
|
||||||
@@ -119,6 +130,59 @@ function switchTab(name) {
|
|||||||
event.currentTarget.classList.add('active');
|
event.currentTarget.classList.add('active');
|
||||||
document.getElementById('tab-' + name).style.display = '';
|
document.getElementById('tab-' + name).style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lightboxItems = [];
|
||||||
|
var lightboxIdx = 0;
|
||||||
|
|
||||||
|
function buildLightboxItems() {
|
||||||
|
lightboxItems = [];
|
||||||
|
var visible = document.querySelectorAll('.tab-panel:not([style*="display:none"]) .showcase-card[data-front]');
|
||||||
|
visible.forEach(function(card) {
|
||||||
|
var front = card.getAttribute('data-front');
|
||||||
|
var number = card.getAttribute('data-number');
|
||||||
|
if (front) lightboxItems.push({src: front, number: number});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLightbox(el) {
|
||||||
|
buildLightboxItems();
|
||||||
|
var front = el.getAttribute('data-front');
|
||||||
|
if (!front) return;
|
||||||
|
var number = el.getAttribute('data-number');
|
||||||
|
for (var i = 0; i < lightboxItems.length; i++) {
|
||||||
|
if (lightboxItems[i].number === number) { lightboxIdx = i; break; }
|
||||||
|
}
|
||||||
|
showLightboxItem();
|
||||||
|
document.getElementById('lightbox').classList.add('active');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLightboxItem() {
|
||||||
|
if (!lightboxItems.length) return;
|
||||||
|
var item = lightboxItems[lightboxIdx];
|
||||||
|
document.getElementById('lightbox-img').src = item.src;
|
||||||
|
document.getElementById('lightbox-caption').textContent = item.number + ' (' + (lightboxIdx+1) + '/' + lightboxItems.length + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLightbox(e) {
|
||||||
|
if (e) e.stopPropagation();
|
||||||
|
document.getElementById('lightbox').classList.remove('active');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function navLightbox(dir, e) {
|
||||||
|
if (e) e.stopPropagation();
|
||||||
|
if (!lightboxItems.length) return;
|
||||||
|
lightboxIdx = (lightboxIdx + dir + lightboxItems.length) % lightboxItems.length;
|
||||||
|
showLightboxItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
var lb = document.getElementById('lightbox');
|
||||||
|
if (!lb || !lb.classList.contains('active')) return;
|
||||||
|
if (e.key === 'Escape') closeLightbox();
|
||||||
|
else if (e.key === 'ArrowLeft') navLightbox(-1);
|
||||||
|
else if (e.key === 'ArrowRight') navLightbox(1);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user