feat(i18n): add English localization with per-user language preference and navbar toggle

This commit is contained in:
2026-07-06 22:07:05 +08:00
parent 172e33e121
commit f448a900f5
17 changed files with 539 additions and 418 deletions

View File

@@ -1,8 +1,8 @@
{% extends "base.html" %}
{% block title %}邀请码管理 - 星笺{% endblock %}
{% block title %}{{ 'invites.title'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
{% block content %}
<div class="section-header">
<h1>邀请码管理</h1>
<h1>{{ 'invites.title'|t(user.language) }}</h1>
</div>
{% if success %}
@@ -14,30 +14,30 @@
<!-- Add new code -->
<div class="form-card" style="margin-bottom:1.5rem">
<h3 style="margin-bottom:.75rem">创建邀请码</h3>
<h3 style="margin-bottom:.75rem">{{ 'invites.create'|t(user.language) }}</h3>
<form method="post" action="/admin/invites/add" style="display:flex;flex-direction:column;gap:.85rem">
<div>
<label style="margin-top:0">邀请码(留空自动生成)</label>
<input type="text" name="code" maxlength="8" placeholder="自动生成" style="text-transform:uppercase">
<label style="margin-top:0">{{ 'invites.code_label'|t(user.language) }}</label>
<input type="text" name="code" maxlength="8" placeholder="{{ 'invites.code_ph'|t(user.language) }}" style="text-transform:uppercase">
</div>
<div>
<label style="margin-top:0">限制类型</label>
<label style="margin-top:0">{{ 'invites.limit_type'|t(user.language) }}</label>
<select name="limit_type" id="limit_type" onchange="toggleLimitFields()">
<option value="unlimited">无限制</option>
<option value="uses">使用次数</option>
<option value="expires">有效期</option>
<option value="unlimited">{{ 'invites.unlimited'|t(user.language) }}</option>
<option value="uses">{{ 'invites.uses'|t(user.language) }}</option>
<option value="expires">{{ 'invites.expires'|t(user.language) }}</option>
</select>
</div>
<div id="max_uses_field" style="display:none;align-items:center;gap:.3rem">
<label style="margin:0;font-size:.8rem;white-space:nowrap">次数</label>
<label style="margin:0;font-size:.8rem;white-space:nowrap">{{ 'invites.times'|t(user.language) }}</label>
<input type="number" name="max_uses" min="1" value="1" class="aif-input aif-short">
</div>
<div id="expires_field" style="display:none;align-items:center;gap:.3rem">
<label style="margin:0;font-size:.8rem;white-space:nowrap">有效期</label>
<label style="margin:0;font-size:.8rem;white-space:nowrap">{{ 'invites.expires'|t(user.language) }}</label>
<input type="datetime-local" name="expires_at" class="aif-input aif-date">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">创建</button>
<button type="submit" class="btn btn-primary">{{ 'invites.create_btn'|t(user.language) }}</button>
</div>
</form>
</div>
@@ -49,41 +49,41 @@
<div class="postcard-row" style="flex-direction:column;align-items:stretch;gap:.75rem">
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
<strong style="font-family:monospace;font-size:1.1rem">{{ c.code }}</strong>
<span style="font-size:.8rem;color:var(--text-muted)">创建者: {{ c.creator.username if c.creator else '-' }}</span>
<span style="font-size:.8rem;color:var(--text-muted)">{{ 'invites.creator'|t(user.language) }}: {{ c.creator.username if c.creator else '-' }}</span>
<span style="font-size:.8rem;color:var(--text-muted)">{{ c.created_at|to_local|strftime('%Y-%m-%d %H:%M') }}</span>
{% if c.max_uses is not none %}
<span class="badge">已用 {{ c.use_count }} / {{ c.max_uses }}</span>
<span class="badge">{{ 'invites.used'|t(user.language) }} {{ c.use_count }} / {{ c.max_uses }}</span>
{% else %}
<span class="badge">已用 {{ c.use_count }} / ∞</span>
<span class="badge">{{ 'invites.used'|t(user.language) }} {{ c.use_count }} / ∞</span>
{% endif %}
{% if c.expires_at %}
<span class="badge">过期: {{ c.expires_at|to_local|strftime('%Y-%m-%d %H:%M') }}</span>
<span class="badge">{{ 'invites.expires_at'|t(user.language) }}: {{ c.expires_at|to_local|strftime('%Y-%m-%d %H:%M') }}</span>
{% else %}
<span class="badge">永不过期</span>
<span class="badge">{{ 'invites.no_expire'|t(user.language) }}</span>
{% endif %}
</div>
{% if c.registered_users %}
<div style="font-size:.8rem;color:var(--text-muted)">注册用户: {{ c.registered_users|map(attribute='username')|join(', ') }}</div>
<div style="font-size:.8rem;color:var(--text-muted)">{{ 'invites.registered_users'|t(user.language) }}: {{ c.registered_users|map(attribute='username')|join(', ') }}</div>
{% endif %}
<form method="post" action="/admin/invites/{{ c.id }}/edit" class="admin-invite-form" id="edit-form-{{ c.id }}">
<div class="aif-field">
<select name="limit_type" class="aif-select" onchange="toggleEditFields({{ c.id }}, this.value)">
<option value="unlimited"{% if c.max_uses is none and not c.expires_at %} selected{% endif %}>无限制</option>
<option value="uses"{% if c.max_uses is not none %} selected{% endif %}>次数限制</option>
<option value="expires"{% if c.expires_at %} selected{% endif %}>有效期</option>
<option value="unlimited"{% if c.max_uses is none and not c.expires_at %} selected{% endif %}>{{ 'invites.unlimited'|t(user.language) }}</option>
<option value="uses"{% if c.max_uses is not none %} selected{% endif %}>{{ 'invites.uses'|t(user.language) }}</option>
<option value="expires"{% if c.expires_at %} selected{% endif %}>{{ 'invites.expires'|t(user.language) }}</option>
</select>
</div>
<div style="display:{% if c.max_uses is not none %}inline-flex{% else %}none{% endif %};align-items:center;gap:.3rem;align-self:flex-end">
<label style="margin:0;font-size:.8rem;white-space:nowrap">次数</label>
<label style="margin:0;font-size:.8rem;white-space:nowrap">{{ 'invites.times'|t(user.language) }}</label>
<input type="number" name="max_uses" value="{{ c.max_uses or 1 }}" min="1" class="aif-input aif-short">
</div>
<div style="display:{% if c.expires_at %}inline-flex{% else %}none{% endif %};align-items:center;gap:.3rem;align-self:flex-end">
<label style="margin:0;font-size:.8rem;white-space:nowrap">有效期</label>
<label style="margin:0;font-size:.8rem;white-space:nowrap">{{ 'invites.expires'|t(user.language) }}</label>
<input type="datetime-local" name="expires_at" value="{{ c.expires_at|to_local|strftime('%Y-%m-%dT%H:%M') if c.expires_at else '' }}" class="aif-input aif-date">
</div>
<div style="display:flex;gap:.4rem;align-self:flex-end">
<button type="submit" class="btn btn-sm">保存</button>
<button type="button" class="btn btn-danger btn-sm" onclick="if(confirm('确定删除?'))this.closest('form').nextElementSibling.submit()">删除</button>
<button type="submit" class="btn btn-sm">{{ 'invites.save'|t(user.language) }}</button>
<button type="button" class="btn btn-danger btn-sm" onclick="if(confirm('{{ 'invites.confirm_delete'|t(user.language) }}'))this.closest('form').nextElementSibling.submit()">{{ 'invites.delete'|t(user.language) }}</button>
</div>
</form>
<form method="post" action="/admin/invites/{{ c.id }}/delete" style="display:none"></form>
@@ -93,7 +93,7 @@
{% else %}
<div class="empty-state">
<div class="empty-icon">🔑</div>
<p>暂无邀请码</p>
<p>{{ 'invites.title'|t(user.language) }}</p>
</div>
{% endif %}

View File

@@ -9,15 +9,19 @@
<body>
{% block nav %}
<nav class="navbar">
<a href="/dashboard" class="nav-brand">📮 星笺</a>
<a href="/dashboard" class="nav-brand">{{ 'brand'|t(user.language if user else 'zh') }}</a>
<div class="nav-links">
{% if user is defined and user %}
<a href="/profiles">Profile</a>
<a href="/settings">设置</a>
<a href="/profiles">{{ 'nav.profiles'|t(user.language) }}</a>
<a href="/settings">{{ 'nav.settings'|t(user.language) }}</a>
{% if user.is_admin %}
<a href="/admin/invites">邀请码管理</a>
<a href="/admin/invites">{{ 'nav.invites'|t(user.language) }}</a>
{% endif %}
<a href="/logout" class="nav-logout">退出</a>
<form method="post" action="/settings/language" class="inline" style="display:inline-flex;align-items:center;gap:0">
<input type="hidden" name="language" value="{{ 'en' if user.language == 'zh' else 'zh' }}">
<button type="submit" class="nav-lang-btn">{{ 'EN' if user.language == 'zh' else '中' }}</button>
</form>
<a href="/logout" class="nav-logout">{{ 'nav.logout'|t(user.language) }}</a>
{% endif %}
</div>
</nav>

View File

@@ -1,8 +1,8 @@
{% extends "base.html" %}
{% block title %}总览 - 星笺{% endblock %}
{% block title %}{{ {'zh':'总览','en':'Dashboard'}[user.language] }} - {{ 'brand'|t(user.language) }}{% endblock %}
{% block content %}
<div class="section-header">
<h1>👋 欢迎,{{ user.username }}</h1>
<h1>{{ 'dash.welcome'|t(user.language) }}{{ user.username }}</h1>
</div>
{% if profiles %}
@@ -11,35 +11,35 @@
<div class="dash-stat-icon">📮</div>
<div class="dash-stat-body">
<span class="dash-stat-value">{{ total }}</span>
<span class="dash-stat-label">全部明信片</span>
<span class="dash-stat-label">{{ 'dash.total_cards'|t(user.language) }}</span>
</div>
</div>
<div class="dash-stat-card">
<div class="dash-stat-icon">📤</div>
<div class="dash-stat-body">
<span class="dash-stat-value dash-sent">{{ sent }}</span>
<span class="dash-stat-label">已寄出</span>
<span class="dash-stat-label">{{ 'dash.sent'|t(user.language) }}</span>
</div>
</div>
<div class="dash-stat-card">
<div class="dash-stat-icon">📬</div>
<div class="dash-stat-body">
<span class="dash-stat-value dash-delivered">{{ delivered }}</span>
<span class="dash-stat-label">已送达</span>
<span class="dash-stat-label">{{ 'dash.delivered'|t(user.language) }}</span>
</div>
</div>
<div class="dash-stat-card">
<div class="dash-stat-icon">💌</div>
<div class="dash-stat-body">
<span class="dash-stat-value dash-received">{{ received }}</span>
<span class="dash-stat-label">已收到</span>
<span class="dash-stat-label">{{ 'dash.received'|t(user.language) }}</span>
</div>
</div>
<div class="dash-stat-card">
<div class="dash-stat-icon">🌍</div>
<div class="dash-stat-body">
<span class="dash-stat-value">{{ countries }}</span>
<span class="dash-stat-label">国家/地区</span>
<span class="dash-stat-label">{{ 'dash.countries'|t(user.language) }}</span>
</div>
</div>
</div>
@@ -52,9 +52,9 @@
<div class="progress-seg progress-received" style="width:{{ received_pct }}%"></div>
</div>
<div class="progress-legend">
<span><i class="dot dot-sent"></i>已寄出 {{ sent_pct }}%</span>
<span><i class="dot dot-delivered"></i>已送达 {{ delivered_pct }}%</span>
<span><i class="dot dot-received"></i>已收到 {{ received_pct }}%</span>
<span><i class="dot dot-sent"></i>{{ 'dash.sent'|t(user.language) }} {{ sent_pct }}%</span>
<span><i class="dot dot-delivered"></i>{{ 'dash.delivered'|t(user.language) }} {{ delivered_pct }}%</span>
<span><i class="dot dot-received"></i>{{ 'dash.received'|t(user.language) }} {{ received_pct }}%</span>
</div>
</div>
{% endif %}
@@ -64,109 +64,62 @@
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1.5rem">
{% if sent_country_stats %}
<div style="background:var(--card-bg);border:1px solid var(--border);border-radius:10px;padding:1.25rem">
<h2 style="margin-bottom:.75rem">📤 寄出</h2>
<h2 style="margin-bottom:.75rem">{{ 'dash.chart_sent'|t(user.language) }}</h2>
<canvas id="chart-sent"></canvas>
</div>
{% endif %}
{% if received_country_stats %}
<div style="background:var(--card-bg);border:1px solid var(--border);border-radius:10px;padding:1.25rem">
<h2 style="margin-bottom:.75rem">📬 收到</h2>
<h2 style="margin-bottom:.75rem">{{ 'dash.chart_received'|t(user.language) }}</h2>
<canvas id="chart-received"></canvas>
</div>
{% endif %}
</div>
<script>
{% if sent_country_stats %}
new Chart(document.getElementById('chart-sent'), {
type: 'bar',
data: {
labels: [{% for code, _ in sent_country_stats %}'{{ code|flag }} {{ code }}',{% endfor %}],
datasets: [{ data: [{% for _, count in sent_country_stats %}{{ count }},{% endfor %}], backgroundColor: '#3b82f6', borderRadius: 4, maxBarThickness: 40 }]
},
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } }, x: { grid: { display: false } } } }
});
{% endif %}
{% if received_country_stats %}
new Chart(document.getElementById('chart-received'), {
type: 'bar',
data: {
labels: [{% for code, _ in received_country_stats %}'{{ code|flag }} {{ code }}',{% endfor %}],
datasets: [{ data: [{% for _, count in received_country_stats %}{{ count }},{% endfor %}], backgroundColor: '#8b5cf6', borderRadius: 4, maxBarThickness: 40 }]
},
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } }, x: { grid: { display: false } } } }
});
{% endif %}
(function() {
var labels, data, colors, elId;
{% for stat in sent_country_stats %}
labels = {{ stat.labels|tojson }};
data = {{ stat.data|tojson }};
colors = labels.map(function(l) { return '#f59e0b'; });
elId = 'chart-sent';
new Chart(document.getElementById(elId), {
type: 'bar',
data: { labels: labels, datasets: [{ data: data, backgroundColor: colors, borderRadius: 4 }] },
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } }
});
{% endfor %}
{% for stat in received_country_stats %}
labels = {{ stat.labels|tojson }};
data = {{ stat.data|tojson }};
colors = labels.map(function(l) { return '#8b5cf6'; });
elId = 'chart-received';
new Chart(document.getElementById(elId), {
type: 'bar',
data: { labels: labels, datasets: [{ data: data, backgroundColor: colors, borderRadius: 4 }] },
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } }
});
{% endfor %}
})();
</script>
{% endif %}
{% if profiles %}
<div class="dash-sections">
<div class="dash-section">
<h2>最近动态</h2>
{% if recent %}
<div class="recent-list">
{% for pc in recent %}
<a href="/postcards/{{ pc.id }}" class="recent-item">
<div class="recent-img">
{% if pc.image_front %}
<img src="{{ pc.image_front }}" alt="" loading="lazy">
{% else %}
<span>📷</span>
{% endif %}
</div>
<div class="recent-info">
<strong>{{ pc.card_number }}</strong>
<span class="badge badge-{{ pc.status }}">{{ {'sent':'已寄出','delivered':'已送达','received':'已收到'}.get(pc.status) }}</span>
</div>
<div class="recent-meta">
{% if pc.status in ('sent','delivered') %}
<span>{{ pc.country|flag }} {{ pc.country or '' }}</span>
<span>→ {{ pc.recipient_name or '-' }}</span>
{% else %}
<span>← {{ pc.sender_name or '-' }}</span>
{% endif %}
<span class="recent-time">
{% if pc.status == 'sent' and pc.send_time %}
{% set delta = (now.date() - pc.send_time.date()).days %}
{% if delta == 0 %}今天{% elif delta == 1 %}昨天{% else %}{{ delta }}天前{% endif %}
{% elif pc.status == 'delivered' and pc.arrival_time %}
{% set delta = (now.date() - pc.arrival_time.date()).days %}
{% if delta == 0 %}今天{% elif delta == 1 %}昨天{% else %}{{ delta }}天前{% endif %}
{% elif pc.status == 'received' and pc.receive_time %}
{% set delta = (now.date() - pc.receive_time.date()).days %}
{% if delta == 0 %}今天{% elif delta == 1 %}昨天{% else %}{{ delta }}天前{% endif %}
{% endif %}
</span>
</div>
</a>
{% endfor %}
{% for p in profiles %}
<a href="/profiles/{{ p.id }}/postcards" class="card dashboard-card">
<h3>{{ p.nickname }}</h3>
<div class="stats">
<span class="stat">{{ p.postcards|length }} {{ 'profiles.total'|t(user.language) }}</span>
</div>
{% else %}
<p class="empty">暂无明信片</p>
{% endif %}
</div>
<div class="dash-section">
<h2>Profile</h2>
<div class="dash-profiles">
{% for p in profiles %}
<a href="/profiles/{{ p.id }}/postcards" class="dash-profile-link">
<span class="dash-profile-name">{{ p.nickname }}</span>
<span class="dash-profile-arrow"></span>
</a>
{% endfor %}
<a href="/profiles" class="dash-profile-link dash-profile-more">
<span class="dash-profile-name">管理全部</span>
<span class="dash-profile-arrow"></span>
</a>
</div>
</div>
</a>
{% endfor %}
</div>
{% endif %}
{% else %}
<div class="empty-state">
<div class="empty-icon">📮</div>
<p>欢迎使用星笺!</p>
<p class="empty-sub">创建一个 <a href="/profiles">Profile</a> 开始管理你的明信片</p>
<p>{{ 'dash.welcome'|t(user.language) }}{{ user.username }} 👋</p>
</div>
{% endif %}
{% endblock %}

View File

@@ -1,21 +1,21 @@
{% extends "base.html" %}
{% block title %}登录 - 星笺{% endblock %}
{% block title %}{{ 'login.title'|t(user.language if user else 'zh') }} - {{ 'brand'|t(user.language if user else 'zh') }}{% endblock %}
{% block content %}
<div class="auth-form">
<h2>登录</h2>
<h2>{{ 'login.title'|t(user.language if user else 'zh') }}</h2>
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<form method="post" action="/login">
<label>用户名</label>
<label>{{ 'login.username'|t(user.language if user else 'zh') }}</label>
<input type="text" name="username" required autofocus>
<label>密码</label>
<label>{{ 'login.password'|t(user.language if user else 'zh') }}</label>
<input type="password" name="password" required>
<label class="checkbox-label" style="margin-top:1rem;display:flex;align-items:center;gap:.4rem;cursor:pointer">
<input type="checkbox" name="remember_me" style="width:auto;margin:0"> 记住我
<input type="checkbox" name="remember_me" style="width:auto;margin:0"> {{ 'login.remember'|t(user.language if user else 'zh') }}
</label>
<button type="submit" class="btn btn-primary">登录</button>
<button type="submit" class="btn btn-primary">{{ 'login.title'|t(user.language if user else 'zh') }}</button>
</form>
<p style="margin-top:1rem;text-align:center;font-size:.85rem">没有账号?<a href="/register">去注册</a></p>
<p style="margin-top:1rem;text-align:center;font-size:.85rem">{{ 'login.no_account'|t(user.language if user else 'zh') }}<a href="/register">{{ 'login.go_register'|t(user.language if user else 'zh') }}</a></p>
</div>
{% endblock %}

View File

@@ -1,67 +1,67 @@
{% extends "base.html" %}
{% block title %}明信片详情 - 星笺{% endblock %}
{% block title %}{{ 'pc_detail.title'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
{% block content %}
<div class="section-header">
<div class="detail-title-row">
<a href="/profiles/{{ postcard.profile_id }}/postcards" class="btn btn-back"></a>
<h1>{{ postcard.card_number }}</h1>
<span class="badge badge-{{ postcard.status }}">{{ {'sent':'已寄出','delivered':'已送达','received':'已收到'}.get(postcard.status, postcard.status) }}</span>
<span class="badge badge-{{ postcard.status }}">{{ {'sent':'已寄出','delivered':'已送达','received':'已收到'}.get(postcard.status, postcard.status) if user.language=='zh' else {'sent':'Sent','delivered':'Delivered','received':'Received'}.get(postcard.status, postcard.status) }}</span>
</div>
<div class="detail-actions">
<a href="/postcards/{{ postcard.id }}/edit" class="btn">编辑</a>
<a href="/postcards/{{ postcard.id }}/edit" class="btn">{{ 'pc_detail.edit'|t(user.language) }}</a>
{% if postcard.status == 'sent' %}
<form method="post" action="/postcards/{{ postcard.id }}/mark-delivered" class="inline">
<button type="submit" class="btn btn-primary" onclick="return confirm('确认已送达?')">已送达</button>
<button type="submit" class="btn btn-primary" onclick="return confirm('{{ 'pc_detail.confirm_delivered'|t(user.language) }}')">{{ 'pc_detail.mark_delivered'|t(user.language) }}</button>
</form>
{% endif %}
<form method="post" action="/postcards/{{ postcard.id }}/delete" class="inline">
<button type="submit" class="btn btn-danger" onclick="return confirm('确定删除?')">删除</button>
<button type="submit" class="btn btn-danger" onclick="return confirm('{{ 'pc_detail.confirm_delete'|t(user.language) }}')">{{ 'pc_detail.delete'|t(user.language) }}</button>
</form>
</div>
</div>
<div class="detail-grid">
<div class="detail-images">
<div class="image-block">
<h3>正面</h3>
<h3>{{ 'pc_detail.front'|t(user.language) }}</h3>
{% if postcard.image_front %}
<img src="{{ postcard.image_front }}" alt="正面" class="detail-img">
<img src="{{ postcard.image_front }}" alt="" class="detail-img">
{% else %}
<div class="detail-img placeholder">📷 未上传正面图片</div>
<div class="detail-img placeholder">{{ 'pc_detail.no_front'|t(user.language) }}</div>
{% endif %}
<form method="post" action="/postcards/{{ postcard.id }}/image/front" enctype="multipart/form-data" class="inline upload-form">
<input type="file" name="file" accept="image/*" required>
<button type="submit" class="btn btn-sm">上传正面</button>
<button type="submit" class="btn btn-sm">{{ 'pc_detail.upload_front'|t(user.language) }}</button>
</form>
</div>
<div class="image-block">
<h3>反面</h3>
<h3>{{ 'pc_detail.back'|t(user.language) }}</h3>
{% if postcard.image_back %}
<img src="{{ postcard.image_back }}" alt="反面" class="detail-img">
<img src="{{ postcard.image_back }}" alt="" class="detail-img">
{% else %}
<div class="detail-img placeholder">📷 未上传反面图片</div>
<div class="detail-img placeholder">{{ 'pc_detail.no_back'|t(user.language) }}</div>
{% endif %}
<form method="post" action="/postcards/{{ postcard.id }}/image/back" enctype="multipart/form-data" class="inline upload-form">
<input type="file" name="file" accept="image/*" required>
<button type="submit" class="btn btn-sm">上传反面</button>
<button type="submit" class="btn btn-sm">{{ 'pc_detail.upload_back'|t(user.language) }}</button>
</form>
</div>
</div>
<div class="detail-meta">
<dl>
<dt>编号</dt><dd>{{ postcard.card_number }}</dd>
<dt>状态</dt><dd>{{ {'sent':'已寄出','delivered':'已送达','received':'已收到'}.get(postcard.status, postcard.status) }}</dd>
<dt>{{ 'pc_detail.card_number'|t(user.language) }}</dt><dd>{{ postcard.card_number }}</dd>
<dt>{{ 'pc_detail.status'|t(user.language) }}</dt><dd>{{ {'sent':'已寄出','delivered':'已送达','received':'已收到'}.get(postcard.status, postcard.status) if user.language=='zh' else {'sent':'Sent','delivered':'Delivered','received':'Received'}.get(postcard.status, postcard.status) }}</dd>
{% if postcard.status in ('sent','delivered') %}
<dt>收件人</dt><dd>{{ postcard.recipient_name or '-' }}</dd>
<dt>国家/地区</dt><dd>{{ postcard.country|flag }} {{ postcard.country or '-' }}</dd>
<dt>寄出时间</dt><dd>{{ postcard.send_time.strftime('%Y-%m-%d') if postcard.send_time else '-' }}</dd>
<dt>到达时间</dt><dd>{{ postcard.arrival_time.strftime('%Y-%m-%d') if postcard.arrival_time else '-' }}</dd>
<dt>{{ 'pc_detail.recipient'|t(user.language) }}</dt><dd>{{ postcard.recipient_name or '-' }}</dd>
<dt>{{ 'pc_detail.country'|t(user.language) }}</dt><dd>{{ postcard.country|flag }} {{ postcard.country or '-' }}</dd>
<dt>{{ 'pc_detail.send_time'|t(user.language) }}</dt><dd>{{ postcard.send_time.strftime('%Y-%m-%d') if postcard.send_time else '-' }}</dd>
<dt>{{ 'pc_detail.arrival_time'|t(user.language) }}</dt><dd>{{ postcard.arrival_time.strftime('%Y-%m-%d') if postcard.arrival_time else '-' }}</dd>
{% else %}
<dt>发件人</dt><dd>{{ postcard.sender_name or '-' }}</dd>
<dt>收到时间</dt><dd>{{ postcard.receive_time.strftime('%Y-%m-%d') if postcard.receive_time else '-' }}</dd>
<dt>{{ 'pc_detail.sender'|t(user.language) }}</dt><dd>{{ postcard.sender_name or '-' }}</dd>
<dt>{{ 'pc_detail.receive_time'|t(user.language) }}</dt><dd>{{ postcard.receive_time.strftime('%Y-%m-%d') if postcard.receive_time else '-' }}</dd>
{% endif %}
<dt>所属 Profile</dt><dd>{{ postcard.profile.nickname }}</dd>
<dt>{{ 'pc_detail.profile'|t(user.language) }}</dt><dd>{{ postcard.profile.nickname }}</dd>
{% if postcard.notes %}
<dt>备注</dt><dd>{{ postcard.notes }}</dd>
<dt>{{ 'pc_detail.notes'|t(user.language) }}</dt><dd>{{ postcard.notes }}</dd>
{% endif %}
</dl>
</div>

View File

@@ -1,44 +1,44 @@
{% extends "base.html" %}
{% block title %}{{ '编辑' if postcard else '新建' }}明信片 - 星笺{% endblock %}
{% block title %}{{ ('pc_form.edit'|t(user.language)) if postcard else ('pc_form.new'|t(user.language)) }} - {{ 'brand'|t(user.language) }}{% endblock %}
{% block content %}
<div class="section-header">
<h1>{{ '编辑' if postcard else '新建' }}明信片</h1>
<h1>{{ ('pc_form.edit'|t(user.language)) if postcard else ('pc_form.new'|t(user.language)) }}</h1>
</div>
<form method="post" action="{{ '/postcards/%d/edit' % postcard.id if postcard else '/profiles/%d/postcards' % profile.id }}" class="form-card">
<label>编号 * <span style="font-weight:400;color:var(--text-muted);font-size:.8rem">格式AB-1234567</span></label>
<label>{{ 'pc_form.number'|t(user.language) }} * <span style="font-weight:400;color:var(--text-muted);font-size:.8rem">{{ 'pc_form.number_hint'|t(user.language) }}</span></label>
<input type="text" name="card_number" value="{{ postcard.card_number if postcard else '' }}" required placeholder="AB-1234567" style="text-transform:uppercase" oninput="this.value=this.value.toUpperCase()">
<label>状态 *</label>
<label>{{ 'pc_form.status'|t(user.language) }} *</label>
<select name="status" id="status-select" onchange="toggleFields()" required>
<option value="sent" {% if postcard and postcard.status=='sent' %}selected{% endif %}>已寄出</option>
<option value="delivered" {% if postcard and postcard.status=='delivered' %}selected{% endif %}>已送达</option>
<option value="received" {% if postcard and postcard.status=='received' %}selected{% endif %}>已收到</option>
<option value="sent" {% if postcard and postcard.status=='sent' %}selected{% endif %}>{{ 'status.sent'|t(user.language) }}</option>
<option value="delivered" {% if postcard and postcard.status=='delivered' %}selected{% endif %}>{{ 'status.delivered'|t(user.language) }}</option>
<option value="received" {% if postcard and postcard.status=='received' %}selected{% endif %}>{{ 'status.received'|t(user.language) }}</option>
</select>
<div id="fields-sent-delivered">
<label>收件人 *</label>
<label>{{ 'pc_form.recipient'|t(user.language) }} *</label>
<input type="text" name="recipient_name" id="input-recipient" value="{{ postcard.recipient_name if postcard else '' }}">
<label>国家/地区 *</label>
<label>{{ 'pc_form.country'|t(user.language) }} *</label>
<input type="text" name="country" id="input-country" value="{{ postcard.country if postcard else '' }}" maxlength="2" placeholder="JP" style="width:80px;text-transform:uppercase" oninput="this.value=this.value.toUpperCase()">
<label>寄出时间 *</label>
<label>{{ 'pc_form.send_time'|t(user.language) }} *</label>
<input type="date" name="send_time" id="input-send-time" value="{{ postcard.send_time.strftime('%Y-%m-%d') if postcard and postcard.send_time else '' }}">
<label id="label-arrival">到达时间</label>
<label id="label-arrival">{{ 'pc_form.arrival_time'|t(user.language) }}</label>
<input type="date" name="arrival_time" id="input-arrival" value="{{ postcard.arrival_time.strftime('%Y-%m-%d') if postcard and postcard.arrival_time else '' }}">
</div>
<div id="fields-received" style="display:none">
<label>发件人 *</label>
<label>{{ 'pc_form.sender'|t(user.language) }} *</label>
<input type="text" name="sender_name" id="input-sender" value="{{ postcard.sender_name if postcard else '' }}">
<label>收到时间 *</label>
<label>{{ 'pc_form.receive_time'|t(user.language) }} *</label>
<input type="date" name="receive_time" id="input-receive" value="{{ postcard.receive_time.strftime('%Y-%m-%d') if postcard and postcard.receive_time else '' }}">
</div>
<label>备注</label>
<textarea name="notes" rows="3" placeholder="可选备注信息">{{ postcard.notes if postcard else '' }}</textarea>
<label>{{ 'pc_form.notes'|t(user.language) }}</label>
<textarea name="notes" rows="3" placeholder="{{ 'pc_form.notes_ph'|t(user.language) }}">{{ postcard.notes if postcard else '' }}</textarea>
<div class="form-actions">
<button type="submit" class="btn btn-primary">保存</button>
<a href="/profiles/{{ profile.id }}/postcards" class="btn">取消</a>
<button type="submit" class="btn btn-primary">{{ 'pc_form.save'|t(user.language) }}</button>
<a href="/profiles/{{ profile.id }}/postcards" class="btn">{{ 'pc_form.cancel'|t(user.language) }}</a>
</div>
</form>
<script>
@@ -60,7 +60,6 @@ function toggleFields() {
country.required = false;
sendTime.required = false;
arrival.required = false;
arrival.disabled = false;
sender.required = true;
receive.required = true;
} else if (s === 'sent') {
@@ -72,7 +71,6 @@ function toggleFields() {
arrival.required = false;
arrival.disabled = true;
arrival.value = '';
labelArrival.textContent = '到达时间';
sender.required = false;
receive.required = false;
} else {
@@ -83,23 +81,19 @@ function toggleFields() {
sendTime.required = true;
arrival.disabled = false;
arrival.required = true;
labelArrival.textContent = '到达时间 *';
sender.required = false;
receive.required = false;
}
}
toggleFields();
{% if not postcard %}
// Auto-fill date fields with today on new postcards
(function() {
var today = new Date().toISOString().slice(0, 10);
var sendTime = document.getElementById('input-send-time');
var receiveTime = document.getElementById('input-receive');
var statusSel = document.getElementById('status-select');
// Fill on page load
if (statusSel.value === 'sent' && !sendTime.value) sendTime.value = today;
if (statusSel.value === 'received' && !receiveTime.value) receiveTime.value = today;
// Fill on status change
statusSel.addEventListener('change', function() {
if (this.value === 'sent' && !sendTime.value) sendTime.value = today;
if (this.value === 'received' && !receiveTime.value) receiveTime.value = today;

View File

@@ -1,9 +1,9 @@
{% extends "base.html" %}
{% block title %}{{ profile.nickname }} - 星笺{% endblock %}
{% block title %}{{ profile.nickname }} - {{ 'brand'|t(user.language) }}{% endblock %}
{% block content %}
<div class="section-header">
<h1>{{ profile.nickname }} 的明信片</h1>
<a href="/profiles/{{ profile.id }}/postcards/new" class="btn btn-primary">+ 新明信片</a>
<h1>{{ profile.nickname }} {{ 'pc_list.title'|t(user.language) }}</h1>
<a href="/profiles/{{ profile.id }}/postcards/new" class="btn btn-primary">{{ 'pc_list.new'|t(user.language) }}</a>
</div>
<div style="margin-bottom:1rem">
{% if profile.notes %}
@@ -13,18 +13,18 @@
<div class="filter-bar">
<form method="get" class="form-row">
<select name="country">
<option value="">全部国家</option>
<option value="">{{ 'pc_list.all_countries'|t(user.language) }}</option>
{% for c in countries %}
<option value="{{ c }}" {% if country == c %}selected{% endif %}>{{ c|flag }} {{ c }}</option>
{% endfor %}
</select>
<button type="submit" class="btn">筛选</button>
<button type="submit" class="btn">{{ 'pc_list.filter'|t(user.language) }}</button>
</form>
</div>
<div class="tabs" id="tabs">
<button class="tab active" onclick="switchTab('sent')">寄出 ({{ sent|length }})</button>
<button class="tab" onclick="switchTab('received')">收到 ({{ received|length }})</button>
<button class="tab active" onclick="switchTab('sent')">{{ 'pc_list.tab_sent'|t(user.language) }} ({{ sent|length }})</button>
<button class="tab" onclick="switchTab('received')">{{ 'pc_list.tab_received'|t(user.language) }} ({{ received|length }})</button>
</div>
<div id="tab-sent" class="tab-panel">
@@ -33,7 +33,7 @@
<a href="/postcards/{{ pc.id }}" class="postcard-row">
<div class="postcard-row-img">
{% if pc.image_front %}
<img src="{{ pc.image_front }}" alt="正面" loading="lazy">
<img src="{{ pc.image_front }}" alt="" loading="lazy">
{% else %}
<div class="thumb placeholder">📷</div>
{% endif %}
@@ -44,24 +44,24 @@
<span>{{ pc.country|flag }} {{ pc.country or '-' }}</span>
<span>→ {{ pc.recipient_name or '-' }}</span>
</div>
<span class="badge badge-{{ pc.status }}">{{ {'sent':'已寄出','delivered':'已送达'}.get(pc.status) }}</span>
<span class="badge badge-{{ pc.status }}">{{ {'sent':'已寄出','delivered':'已送达'}.get(pc.status) if user.language=='zh' else {'sent':'Sent','delivered':'Delivered'}.get(pc.status) }}</span>
<span class="postcard-row-date">
{% if pc.send_time %}{{ pc.send_time.strftime('%Y-%m-%d') }}寄出{% endif %}
{% if pc.arrival_time %}<br>{{ pc.arrival_time.strftime('%Y-%m-%d') }}送达{% endif %}
{% if pc.send_time %}{{ pc.send_time.strftime('%Y-%m-%d') }}{% if user.language=='zh' %}寄出{% endif %}{% endif %}
{% if pc.arrival_time %}<br>{{ pc.arrival_time.strftime('%Y-%m-%d') }}{% if user.language=='zh' %}送达{% endif %}{% endif %}
</span>
</div>
{% if pc.notes %}<span class="postcard-row-notes">{{ pc.notes[:50] }}{% if pc.notes|length > 50 %}…{% endif %}</span>{% endif %}
<div class="postcard-row-action">
{% if pc.status == 'sent' %}
<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">已送达</button>
<form method="post" action="/postcards/{{ pc.id }}/mark-delivered" onclick="event.preventDefault();event.stopPropagation();if(confirm('{{ 'pc_list.confirm_delivered'|t(user.language) }}'))this.submit();">
<button type="submit" class="btn btn-sm btn-primary">{{ 'pc_list.mark_delivered'|t(user.language) }}</button>
</form>
{% endif %}
</div>
</a>
{% endfor %}
{% else %}
<p class="empty">没有寄出的明信片。</p>
<p class="empty">{{ 'pc_list.empty_sent'|t(user.language) }}</p>
{% endif %}
</div>
@@ -71,7 +71,7 @@
<a href="/postcards/{{ pc.id }}" class="postcard-row">
<div class="postcard-row-img">
{% if pc.image_front %}
<img src="{{ pc.image_front }}" alt="正面" loading="lazy">
<img src="{{ pc.image_front }}" alt="" loading="lazy">
{% else %}
<div class="thumb placeholder">📷</div>
{% endif %}
@@ -81,13 +81,13 @@
<div class="postcard-row-detail">
<span>← {{ pc.sender_name or '-' }}</span>
</div>
{% if pc.receive_time %}<span class="postcard-row-date">{{ pc.receive_time.strftime('%Y-%m-%d') }}收到</span>{% endif %}
{% if pc.receive_time %}<span class="postcard-row-date">{{ pc.receive_time.strftime('%Y-%m-%d') }}{% if user.language=='zh' %}收到{% endif %}</span>{% endif %}
</div>
{% if pc.notes %}<span class="postcard-row-notes">{{ pc.notes[:50] }}{% if pc.notes|length > 50 %}…{% endif %}</span>{% endif %}
</a>
{% endfor %}
{% else %}
<p class="empty">没有收到的明信片。</p>
<p class="empty">{{ 'pc_list.empty_received'|t(user.language) }}</p>
{% endif %}
</div>

View File

@@ -1,28 +1,28 @@
{% extends "base.html" %}
{% block title %}{{ profile.nickname }} 展示设置 - 星笺{% endblock %}
{% block title %}{{ profile.nickname }} {{ 'profile_showcase.title'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
{% block content %}
<div class="section-header">
<div class="detail-title-row">
<a href="/settings?tab=showcase" class="btn btn-back"></a>
<h1>{{ profile.nickname }} 展示设置</h1>
<h1>{{ profile.nickname }} {{ 'profile_showcase.title'|t(user.language) }}</h1>
</div>
</div>
<div class="form-card">
<form method="post" action="/profiles/{{ profile.id }}/showcase" style="display:flex;flex-wrap:wrap;gap:1rem;align-items:center">
<div style="display:flex;align-items:center;gap:.5rem">
<label style="margin:0;font-size:.85rem">开启展示</label>
<label style="margin:0;font-size:.85rem">{{ 'profile_showcase.enable'|t(user.language) }}</label>
<label class="toggle-switch">
<input type="checkbox" name="showcase_enabled" value="1" {% if profile.showcase_enabled %}checked{% endif %} onchange="this.form.submit()">
<span class="toggle-slider"></span>
</label>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<label style="margin:0;font-size:.85rem">展示内容</label>
<label style="margin:0;font-size:.85rem">{{ 'profile_showcase.content'|t(user.language) }}</label>
<select name="showcase_visible" onchange="this.form.submit()" style="width:auto">
<option value="both" {% if profile.showcase_visible == 'both' %}selected{% endif %}>发出 + 收到</option>
<option value="sent" {% if profile.showcase_visible == 'sent' %}selected{% endif %}>仅发出</option>
<option value="received" {% if profile.showcase_visible == 'received' %}selected{% endif %}>仅收到</option>
<option value="both" {% if profile.showcase_visible == 'both' %}selected{% endif %}>{{ 'profile_showcase.both'|t(user.language) }}</option>
<option value="sent" {% if profile.showcase_visible == 'sent' %}selected{% endif %}>{{ 'profile_showcase.sent_only'|t(user.language) }}</option>
<option value="received" {% if profile.showcase_visible == 'received' %}selected{% endif %}>{{ 'profile_showcase.received_only'|t(user.language) }}</option>
</select>
</div>
</form>
@@ -30,19 +30,19 @@
{% if profile.showcase_enabled and cards %}
<div style="margin-top:1.5rem">
<h2 style="font-size:1rem;margin-bottom:1rem">单张明信片展示控制</h2>
<h2 style="font-size:1rem;margin-bottom:1rem">{{ 'profile_showcase.single_control'|t(user.language) }}</h2>
<div style="background:var(--card-bg);border:1px solid var(--border);border-radius:10px;padding:1rem">
{% for pc in cards %}
<form method="post" action="/postcards/{{ pc.id }}/showcase-toggle" style="display:inline-flex;align-items:center;gap:.4rem;margin-right:1rem;margin-bottom:.4rem;font-size:.85rem">
<span>{{ pc.card_number }}</span>
<span class="badge" style="font-size:.7rem;{% if pc.status == 'sent' %}background:var(--sent){% elif pc.status == 'delivered' %}background:var(--delivered){% else %}background:var(--received){% endif %}">
{{ {'sent':'寄出','delivered':'送达','received':'收到'}.get(pc.status, pc.status) }}
{{ {'sent':'寄出','delivered':'送达','received':'收到'}.get(pc.status, pc.status) if user.language=='zh' else {'sent':'Sent','delivered':'Delivered','received':'Received'}.get(pc.status, pc.status) }}
</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>
{% if pc.showcase_hidden %}<span style="color:var(--text-muted);font-size:.75rem">隐藏</span>{% endif %}
{% if pc.showcase_hidden %}<span style="color:var(--text-muted);font-size:.75rem">{{ 'profile_showcase.hidden'|t(user.language) }}</span>{% endif %}
</form>
{% endfor %}
</div>
@@ -51,7 +51,7 @@
{% if not profile.showcase_enabled %}
<div class="empty-state" style="margin-top:2rem">
<p>开启展示后可控制单张明信片的可见性</p>
<p>{{ 'profile_showcase.hint'|t(user.language) }}</p>
</div>
{% endif %}
{% endblock %}

View File

@@ -1,13 +1,13 @@
{% extends "base.html" %}
{% block title %}Profile - 星笺{% endblock %}
{% block title %}{{ 'nav.profiles'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
{% block content %}
<div class="section-header">
<h1>Profile</h1>
<h1>{{ 'nav.profiles'|t(user.language) }}</h1>
</div>
<div class="inline-form">
<form method="post" action="/profiles" class="form-row">
<input type="text" name="nickname" placeholder="新 Profile 昵称" required>
<button type="submit" class="btn btn-primary">+ 新建</button>
<input type="text" name="nickname" placeholder="{{ 'profiles.new_ph'|t(user.language) }}" required>
<button type="submit" class="btn btn-primary">{{ 'profiles.new_btn'|t(user.language) }}</button>
</form>
</div>
{% if profiles %}
@@ -18,7 +18,7 @@
<div class="profile-card-header">
<h3 class="profile-card-name">
<span id="name-display-{{ p.id }}">{{ p.nickname }}</span>
<button type="button" class="icon-btn" onclick="startRename({{ p.id }})" title="改名">✏️</button>
<button type="button" class="icon-btn" onclick="startRename({{ p.id }})" title="{{ 'profiles.rename'|t(user.language) }}">✏️</button>
</h3>
<form method="post" action="/profiles/{{ p.id }}/rename" class="rename-form" id="name-edit-{{ p.id }}" style="display:none">
<input type="text" name="nickname" value="{{ p.nickname }}" required>
@@ -29,61 +29,54 @@
<div class="profile-card-stats">
<div class="stat">
<span class="stat-value">{{ e.total }}</span>
<span class="stat-label">总计</span>
<span class="stat-label">{{ 'profiles.total'|t(user.language) }}</span>
</div>
<div class="stat">
<span class="stat-value badge-sent-text">{{ e.sent }}</span>
<span class="stat-label">已寄出</span>
<span class="stat-label">{{ 'dash.sent'|t(user.language) }}</span>
</div>
<div class="stat">
<span class="stat-value badge-delivered-text">{{ e.delivered }}</span>
<span class="stat-label">已送达</span>
<span class="stat-label">{{ 'dash.delivered'|t(user.language) }}</span>
</div>
<div class="stat">
<span class="stat-value badge-received-text">{{ e.received }}</span>
<span class="stat-label">已收到</span>
<span class="stat-label">{{ 'dash.received'|t(user.language) }}</span>
</div>
<div class="stat">
<span class="stat-value">{{ e.countries }}</span>
<span class="stat-label">国家</span>
<span class="stat-label">{{ 'profiles.countries'|t(user.language) }}</span>
</div>
</div>
<div class="profile-card-notes" style="padding:.5rem .75rem">
<form method="post" action="/profiles/{{ p.id }}/notes" style="display:flex;align-items:center;gap:.4rem">
<textarea name="notes" rows="1" placeholder="备注(仅自己可见)" style="flex:1;padding:.3rem .5rem;border:1px solid var(--border);border-radius:6px;font-size:.8rem;resize:none;font-family:inherit;line-height:1.4;min-height:1.6rem">{{ p.notes }}</textarea>
<button type="submit" class="btn btn-sm" style="flex-shrink:0;margin-top:0">保存</button>
<textarea name="notes" rows="1" placeholder="{{ 'profiles.notes_ph'|t(user.language) }}" style="flex:1;padding:.3rem .5rem;border:1px solid var(--border);border-radius:6px;font-size:.8rem;resize:none;font-family:inherit;line-height:1.4;min-height:1.6rem">{{ p.notes }}</textarea>
<button type="submit" class="btn btn-sm" style="flex-shrink:0;margin-top:0">{{ 'profiles.save'|t(user.language) }}</button>
</form>
</div>
<div class="profile-card-footer">
<span class="profile-card-date">创建于 {{ p.created_at.strftime('%Y-%m-%d') }}</span>
<div class="profile-card-actions">
<a href="/profiles/{{ p.id }}/postcards" class="btn btn-primary btn-sm">查看</a>
<form method="post" action="/profiles/{{ p.id }}/delete" class="inline">
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('确定删除「{{ p.nickname }}」及其所有明信片?')">删除</button>
</form>
</div>
<span class="profile-card-date">{{ 'profiles.created_at'|t(user.language) }} {{ p.created_at.strftime('%Y-%m-%d') }}</span>
<a href="/profiles/{{ p.id }}/postcards" class="btn btn-sm">{{ 'profiles.view'|t(user.language) }}</a>
<form method="post" action="/profiles/{{ p.id }}/delete" class="inline">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('{{ 'profiles.confirm_delete'|t(user.language) }}')">{{ 'profiles.delete'|t(user.language) }}</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<div class="empty-icon">📮</div>
<p>还没有 Profile</p>
<p class="empty-sub">创建一个 Profile 来管理你的明信片吧</p>
</div>
<p class="empty">{{ 'profiles.new_ph'|t(user.language) }}</p>
{% endif %}
<script>
function startRename(id) {
document.getElementById('name-display-' + id).style.display = 'none';
var form = document.getElementById('name-edit-' + id);
form.style.display = 'inline-flex';
form.querySelector('input').focus();
document.getElementById('name-edit-' + id).style.display = '';
document.getElementById('name-edit-' + id).querySelector('input').focus();
}
function cancelRename(id) {
var form = document.getElementById('name-edit-' + id);
form.style.display = 'none';
document.getElementById('name-display-' + id).style.display = '';
document.getElementById('name-edit-' + id).style.display = 'none';
}
</script>
{% endblock %}

View File

@@ -1,23 +1,23 @@
{% extends "base.html" %}
{% block title %}注册 - 星笺{% endblock %}
{% block title %}{{ 'register.title'|t(user.language if user else 'zh') }} - {{ 'brand'|t(user.language if user else 'zh') }}{% endblock %}
{% block content %}
<div class="auth-form">
<h2>注册</h2>
<p style="color:var(--text-muted);font-size:.85rem;margin-bottom:1rem">邀请码由已有用户或管理员提供</p>
<h2>{{ 'register.title'|t(user.language if user else 'zh') }}</h2>
<p style="color:var(--text-muted);font-size:.85rem;margin-bottom:1rem">{{ 'register.hint'|t(user.language if user else 'zh') }}</p>
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<form method="post" action="/register">
<label>邀请码</label>
<input type="text" name="invite_code" required maxlength="8" placeholder="8位字母数字" style="text-transform:none" autofocus>
<label>用户名</label>
<label>{{ 'register.invite_code'|t(user.language if user else 'zh') }}</label>
<input type="text" name="invite_code" required maxlength="8" placeholder="{{ 'register.invite_ph'|t(user.language if user else 'zh') }}" style="text-transform:none" autofocus>
<label>{{ 'register.username'|t(user.language if user else 'zh') }}</label>
<input type="text" name="username" required maxlength="64">
<label>密码</label>
<label>{{ 'register.password'|t(user.language if user else 'zh') }}</label>
<input type="password" name="password" required minlength="6">
<label>确认密码</label>
<label>{{ 'register.confirm_password'|t(user.language if user else 'zh') }}</label>
<input type="password" name="confirm_password" required>
<button type="submit" class="btn btn-primary">注册</button>
<button type="submit" class="btn btn-primary">{{ 'register.title'|t(user.language if user else 'zh') }}</button>
</form>
<p style="margin-top:1rem;text-align:center;font-size:.85rem">已有账号?<a href="/login">去登录</a></p>
<p style="margin-top:1rem;text-align:center;font-size:.85rem">{{ 'register.has_account'|t(user.language if user else 'zh') }}<a href="/login">{{ 'register.go_login'|t(user.language if user else 'zh') }}</a></p>
</div>
{% endblock %}

View File

@@ -1,13 +1,13 @@
{% extends "base.html" %}
{% block title %}用户设置 - 星笺{% endblock %}
{% block title %}{{ 'settings.title'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
{% block content %}
<div class="section-header">
<h1>用户设置</h1>
<h1>{{ 'settings.title'|t(user.language) }}</h1>
</div>
<div class="tabs" id="tabs">
<button class="tab {% if active_tab != 'showcase' %}active{% endif %}" onclick="switchTab('account')">账号</button>
<button class="tab {% if active_tab == 'showcase' %}active{% endif %}" onclick="switchTab('showcase')">展示</button>
<button class="tab {% if active_tab != 'showcase' %}active{% endif %}" onclick="switchTab('account')">{{ 'settings.tab_account'|t(user.language) }}</button>
<button class="tab {% if active_tab == 'showcase' %}active{% endif %}" onclick="switchTab('showcase')">{{ 'settings.tab_showcase'|t(user.language) }}</button>
</div>
{% if success %}
@@ -19,129 +19,94 @@
<div id="tab-account" class="tab-panel" {% if active_tab == 'showcase' %}style="display:none"{% endif %}>
<div class="form-card">
<h3 style="margin-bottom:.75rem">修改用户名</h3>
<h3 style="margin-bottom:.75rem">{{ 'settings.change_username'|t(user.language) }}</h3>
<form method="post" action="/settings/change-username">
<label>当前用户名</label>
<label>{{ 'settings.current_username'|t(user.language) }}</label>
<input type="text" value="{{ user.username }}" disabled>
<label>新用户名</label>
<input type="text" name="new_username" required maxlength="64" placeholder="输入新用户名">
<label>{{ 'settings.new_username'|t(user.language) }}</label>
<input type="text" name="new_username" required maxlength="64">
<div class="form-actions">
<button type="submit" class="btn btn-primary">修改用户名</button>
<button type="submit" class="btn btn-primary">{{ 'settings.change_username'|t(user.language) }}</button>
</div>
</form>
</div>
<div class="form-card" style="margin-top:1.5rem">
<h3 style="margin-bottom:.75rem">修改密码</h3>
<h3 style="margin-bottom:.75rem">{{ 'settings.change_password'|t(user.language) }}</h3>
<form method="post" action="/settings/change-password">
<label>当前密码</label>
<input type="password" name="old_password" required placeholder="输入当前密码">
<label>新密码</label>
<input type="password" name="new_password" required placeholder="输入新密码">
<label>确认新密码</label>
<input type="password" name="confirm_password" required placeholder="再次输入新密码">
<label>{{ 'settings.current_password'|t(user.language) }}</label>
<input type="password" name="old_password" required>
<label>{{ 'settings.new_password'|t(user.language) }}</label>
<input type="password" name="new_password" required>
<label>{{ 'settings.confirm_password'|t(user.language) }}</label>
<input type="password" name="confirm_password" required>
<div class="form-actions">
<button type="submit" class="btn btn-primary">修改密码</button>
<button type="submit" class="btn btn-primary">{{ 'settings.change_password'|t(user.language) }}</button>
</div>
</form>
</div>
<div class="form-card" style="margin-top:1.5rem">
<h3 style="margin-bottom:.75rem">{{ 'settings.language'|t(user.language) }}</h3>
<form method="post" action="/settings/language" style="display:flex;align-items:center;gap:.75rem;flex-wrap:wrap">
<select name="language" style="width:auto">
<option value="zh" {% if user.language == 'zh' %}selected{% endif %}>{{ 'settings.language_zh'|t(user.language) }}</option>
<option value="en" {% if user.language == 'en' %}selected{% endif %}>{{ 'settings.language_en'|t(user.language) }}</option>
</select>
<button type="submit" class="btn btn-primary btn-sm">{{ 'settings.save'|t(user.language) }}</button>
</form>
</div>
</div>
<div id="tab-showcase" class="tab-panel" {% if active_tab != 'showcase' %}style="display:none"{% endif %}>
<div class="form-card">
<h3 style="margin-bottom:.75rem">全局展示设置</h3>
<h3 style="margin-bottom:.75rem">{{ 'settings.showcase_global'|t(user.language) }}</h3>
<form method="post" action="/showcase/manage">
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
<label style="margin:0;white-space:nowrap">展示页面模式</label>
<label style="margin:0;white-space:nowrap">{{ 'settings.showcase_mode'|t(user.language) }}</label>
<select name="showcase_mode" style="width:auto">
<option value="profile" {% if user.showcase_mode == 'profile' %}selected{% endif %}>按 Profile 分类</option>
<option value="flat" {% if user.showcase_mode == 'flat' %}selected{% endif %}>全部合并</option>
<option value="profile" {% if user.showcase_mode == 'profile' %}selected{% endif %}>{{ 'settings.mode_profile'|t(user.language) }}</option>
<option value="flat" {% if user.showcase_mode == 'flat' %}selected{% endif %}>{{ 'settings.mode_flat'|t(user.language) }}</option>
</select>
<button type="submit" class="btn btn-primary btn-sm">保存</button>
<button type="submit" class="btn btn-primary btn-sm">{{ 'settings.save'|t(user.language) }}</button>
</div>
</form>
<p style="margin-top:.75rem;font-size:.8rem;color:var(--text-muted)">
公开展示页:<a href="/u/{{ user.username }}" target="_blank">/u/{{ user.username }}</a>
{{ 'settings.public_url_label'|t(user.language) }}<a href="/u/{{ user.username }}" target="_blank">/u/{{ user.username }}</a>
</p>
</div>
{% if profiles %}
<div style="margin-top:1.5rem">
<h2 style="font-size:1rem;margin-bottom:1rem">各 Profile 展示设置</h2>
{% if profiles %}
<h2 style="font-size:1rem;margin-bottom:1rem">{{ 'settings.profiles_showcase'|t(user.language) }}</h2>
{% for p in profiles %}
{% set sent_cards = [] %}
{% set received_cards = [] %}
{% if profile_cards and p.id in profile_cards %}
{% for pc in profile_cards[p.id] %}
{% if pc.status in ('sent', 'delivered') %}
{% if sent_cards.append(pc) %}{% endif %}
{% else %}
{% if received_cards.append(pc) %}{% endif %}
{% endif %}
{% endfor %}
{% endif %}
<div class="showcase-profile-card" style="background:var(--card-bg);border:1px solid var(--border);border-radius:10px;padding:1rem;margin-bottom:.75rem">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem">
<div class="form-card" style="margin-bottom:.75rem">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem">
<strong>{{ p.nickname }}</strong>
<span class="badge" style="{% if p.showcase_enabled %}background:var(--sent){% else %}background:var(--text-muted){% endif %}">{% if p.showcase_enabled %}展示中{% else %}未开启{% endif %}</span>
<span class="badge">{% if p.showcase_enabled %}{{ 'settings.showcase_on'|t(user.language) }}{% else %}{{ 'settings.showcase_off'|t(user.language) }}{% endif %}</span>
</div>
<form method="post" action="/profiles/{{ p.id }}/showcase">
<div style="display:flex;flex-wrap:wrap;gap:1rem;align-items:center">
<div style="display:flex;align-items:center;gap:.5rem">
<label style="margin:0;font-size:.85rem">开启展示</label>
<label class="toggle-switch">
<input type="checkbox" name="showcase_enabled" value="1" {% if p.showcase_enabled %}checked{% endif %} onchange="this.form.submit()">
<span class="toggle-slider"></span>
</label>
</div>
<div style="display:flex;align-items:center;gap:.5rem">
<label style="margin:0;font-size:.85rem">展示内容</label>
<select name="showcase_visible" onchange="this.form.submit()" style="width:auto">
<option value="both" {% if p.showcase_visible == 'both' %}selected{% endif %}>发出 + 收到</option>
<option value="sent" {% if p.showcase_visible == 'sent' %}selected{% endif %}>仅发出</option>
<option value="received" {% if p.showcase_visible == 'received' %}selected{% endif %}>仅收到</option>
</select>
</div>
<form method="post" action="/profiles/{{ p.id }}/showcase" style="display:flex;flex-wrap:wrap;gap:.75rem;align-items:flex-end">
<div style="display:flex;flex-direction:column;gap:.25rem">
<label style="margin:0;font-size:.78rem">{{ 'settings.enable_showcase'|t(user.language) }}</label>
<label class="toggle-switch">
<input type="checkbox" name="showcase_enabled" value="1" {% if p.showcase_enabled %}checked{% endif %} onchange="this.form.submit()">
<span class="toggle-slider"></span>
</label>
</div>
<div style="display:flex;flex-direction:column;gap:.25rem">
<label style="margin:0;font-size:.78rem">{{ 'settings.showcase_content'|t(user.language) }}</label>
<select name="showcase_visible" onchange="this.form.submit()">
<option value="both" {% if p.showcase_visible == 'both' %}selected{% endif %}>{{ 'settings.content_both'|t(user.language) }}</option>
<option value="sent" {% if p.showcase_visible == 'sent' %}selected{% endif %}>{{ 'settings.content_sent'|t(user.language) }}</option>
<option value="received" {% if p.showcase_visible == 'received' %}selected{% endif %}>{{ 'settings.content_received'|t(user.language) }}</option>
</select>
</div>
<a href="/profiles/{{ p.id }}/showcase" class="btn btn-sm">{{ 'settings.save'|t(user.language) }}</a>
</form>
{% if p.showcase_enabled and (sent_cards or received_cards) %}
<div style="margin-top:.75rem;border-top:1px solid var(--border);padding-top:.75rem">
<p style="font-size:.8rem;color:var(--text-muted);margin-bottom:.5rem">单张明信片展示控制:</p>
{% if sent_cards %}
<div style="margin-bottom:.4rem">
<span style="font-size:.78rem;color:var(--text-muted);margin-right:.5rem">发出</span>
{% 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">
<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>
</form>
{% endfor %}
</div>
{% endif %}
{% if received_cards %}
<div>
<span style="font-size:.78rem;color:var(--text-muted);margin-right:.5rem">收到</span>
{% 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">
<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>
</form>
{% endfor %}
</div>
{% endif %}
</div>
{% elif p.showcase_enabled %}
<p style="margin-top:.75rem;font-size:.8rem;color:var(--text-muted)">暂无明信片</p>
{% endif %}
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<p>暂无 Profile请先创建</p>
</div>
{% endif %}
</div>
{% endif %}
</div>
<script>
@@ -151,5 +116,8 @@ function switchTab(name) {
event.currentTarget.classList.add('active');
document.getElementById('tab-' + name).style.display = '';
}
{% if active_tab == 'showcase' %}
document.querySelector('[onclick*="showcase"]').click();
{% endif %}
</script>
{% endblock %}

View File

@@ -1,21 +1,22 @@
{% extends "base.html" %}
{% block title %}{{ profile_user.username }} 的明信片{% endblock %}
{% set lang = lang if lang is defined else 'zh' %}
{% block title %}{{ profile_user.username }} {{ 'showcase.title'|t(lang) }}{% endblock %}
{% block nav %}{% endblock %}
{% block content %}
<div class="section-header">
<h1>📮 {{ profile_user.username }} 的明信片</h1>
<h1>📮 {{ profile_user.username }} {{ 'showcase.title'|t(lang) }}</h1>
</div>
{% set has_content = (sent_all|length + received_all|length) > 0 %}
{% if not has_content %}
<div class="empty-state">
<p>暂无展示内容</p>
<p>{{ 'showcase.empty'|t(lang) }}</p>
</div>
{% else %}
<div class="tabs" id="tabs">
<button class="tab active" onclick="switchTab('sent')">发出 ({{ sent_all|length }})</button>
<button class="tab" onclick="switchTab('received')">收到 ({{ received_all|length }})</button>
<button class="tab active" onclick="switchTab('sent')">{{ 'showcase.tab_sent'|t(lang) }} ({{ sent_all|length }})</button>
<button class="tab" onclick="switchTab('received')">{{ 'showcase.tab_received'|t(lang) }} ({{ received_all|length }})</button>
</div>
<div id="tab-sent" class="tab-panel">
@@ -113,76 +114,47 @@
</div>
{% endif %}
<div id="lightbox" class="lightbox" onclick="closeLightbox(event)">
<button class="lightbox-close" onclick="closeLightbox(event)">&times;</button>
<button class="lightbox-prev" onclick="navLightbox(-1,event)">&#10094;</button>
<button class="lightbox-next" onclick="navLightbox(1,event)">&#10095;</button>
<!-- Lightbox -->
<div id="lightbox" class="lightbox" onclick="closeLightbox()">
<button class="lightbox-close" onclick="closeLightbox()"></button>
<button class="lightbox-prev" onclick="event.stopPropagation();navLightbox(-1)"></button>
<button class="lightbox-next" onclick="event.stopPropagation();navLightbox(1)"></button>
<div class="lightbox-content" onclick="event.stopPropagation()">
<img id="lightbox-img" src="" alt="">
<div id="lightbox-caption" class="lightbox-caption"></div>
<div id="lightbox-number" class="lightbox-number"></div>
</div>
</div>
<script>
var cards = [];
document.querySelectorAll('.showcase-card').forEach(function(el) {
cards.push({ front: el.dataset.front, number: el.dataset.number });
});
var currentIndex = 0;
function openLightbox(el) {
currentIndex = cards.findIndex(function(c) { return c.number === el.dataset.number; });
showLightbox();
document.getElementById('lightbox').style.display = 'flex';
}
function showLightbox() {
var c = cards[currentIndex];
document.getElementById('lightbox-img').src = c.front;
document.getElementById('lightbox-number').textContent = c.number;
}
function closeLightbox() { document.getElementById('lightbox').style.display = 'none'; }
function navLightbox(dir) { currentIndex = (currentIndex + dir + cards.length) % cards.length; showLightbox(); }
document.addEventListener('keydown', function(e) {
if (document.getElementById('lightbox').style.display !== 'flex') return;
if (e.key === 'Escape') closeLightbox();
if (e.key === 'ArrowLeft') navLightbox(-1);
if (e.key === 'ArrowRight') navLightbox(1);
});
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 = '';
}
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>
{% endblock %}

View File

@@ -1,60 +1,60 @@
{% extends "base.html" %}
{% block title %}展示管理 - 星笺{% endblock %}
{% block title %}{{ 'showcase_manage.title'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
{% block content %}
<div class="section-header">
<div class="detail-title-row">
<a href="/profiles" class="btn btn-back"></a>
<h1>展示管理</h1>
<h1>{{ 'showcase_manage.title'|t(user.language) }}</h1>
</div>
</div>
<div class="form-card">
<h3 style="margin-bottom:.75rem">全局展示设置</h3>
<h3 style="margin-bottom:.75rem">{{ 'showcase_manage.global'|t(user.language) }}</h3>
<form method="post" action="/showcase/manage">
<label>展示页面模式</label>
<label>{{ 'settings.showcase_mode'|t(user.language) }}</label>
<select name="showcase_mode">
<option value="profile" {% if user.showcase_mode == 'profile' %}selected{% endif %}>按 Profile 分类</option>
<option value="flat" {% if user.showcase_mode == 'flat' %}selected{% endif %}>全部合并</option>
<option value="profile" {% if user.showcase_mode == 'profile' %}selected{% endif %}>{{ 'settings.mode_profile'|t(user.language) }}</option>
<option value="flat" {% if user.showcase_mode == 'flat' %}selected{% endif %}>{{ 'settings.mode_flat'|t(user.language) }}</option>
</select>
<div class="form-actions">
<button type="submit" class="btn btn-primary">保存全局设置</button>
<button type="submit" class="btn btn-primary">{{ 'showcase_manage.save'|t(user.language) }}</button>
</div>
</form>
<p class="form-hint" style="margin-top:.5rem;font-size:.8rem;color:var(--text-muted)">
公开展示页地址:<a href="/u/{{ user.username }}" target="_blank">/u/{{ user.username }}</a>
{{ 'showcase_manage.public_url'|t(user.language) }}<a href="/u/{{ user.username }}" target="_blank">/u/{{ user.username }}</a>
</p>
</div>
<div style="margin-top:1.5rem">
<h2 style="font-size:1rem;margin-bottom:1rem">各 Profile 展示设置</h2>
<h2 style="font-size:1rem;margin-bottom:1rem">{{ 'showcase_manage.profiles'|t(user.language) }}</h2>
{% if profiles %}
{% for p in profiles %}
{% set pc_list = profile_cards.get(p.id, []) %}
<div class="showcase-profile" style="background:var(--card-bg);border:1px solid var(--border);border-radius:10px;padding:1rem;margin-bottom:.75rem">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem">
<strong>{{ p.nickname }}</strong>
<span class="badge" style="{% if p.showcase_enabled %}background:var(--sent){% else %}background:var(--text-muted){% endif %}">{% if p.showcase_enabled %}展示中{% else %}未开启{% endif %}</span>
<span class="badge" style="{% if p.showcase_enabled %}background:var(--sent){% else %}background:var(--text-muted){% endif %}">{% if p.showcase_enabled %}{{ 'showcase_manage.on'|t(user.language) }}{% else %}{{ 'showcase_manage.off'|t(user.language) }}{% endif %}</span>
</div>
<form method="post" action="/profiles/{{ p.id }}/showcase" style="display:flex;flex-wrap:wrap;gap:.75rem;align-items:flex-end">
<div style="display:flex;flex-direction:column;gap:.25rem">
<label style="margin:0;font-size:.78rem">开启展示</label>
<label style="margin:0;font-size:.78rem">{{ 'showcase_manage.enable'|t(user.language) }}</label>
<label class="toggle-switch">
<input type="checkbox" name="showcase_enabled" value="1" {% if p.showcase_enabled %}checked{% endif %} onchange="this.form.submit()">
<span class="toggle-slider"></span>
</label>
</div>
<div style="display:flex;flex-direction:column;gap:.25rem">
<label style="margin:0;font-size:.78rem">展示内容</label>
<label style="margin:0;font-size:.78rem">{{ 'showcase_manage.content'|t(user.language) }}</label>
<select name="showcase_visible" onchange="this.form.submit()">
<option value="both" {% if p.showcase_visible == 'both' %}selected{% endif %}>发出 + 收到</option>
<option value="sent" {% if p.showcase_visible == 'sent' %}selected{% endif %}>仅发出</option>
<option value="received" {% if p.showcase_visible == 'received' %}selected{% endif %}>仅收到</option>
<option value="both" {% if p.showcase_visible == 'both' %}selected{% endif %}>{{ 'showcase_manage.both'|t(user.language) }}</option>
<option value="sent" {% if p.showcase_visible == 'sent' %}selected{% endif %}>{{ 'showcase_manage.sent_only'|t(user.language) }}</option>
<option value="received" {% if p.showcase_visible == 'received' %}selected{% endif %}>{{ 'showcase_manage.received_only'|t(user.language) }}</option>
</select>
</div>
</form>
{% if p.showcase_enabled and pc_list %}
<div style="margin-top:.75rem;border-top:1px solid var(--border);padding-top:.75rem">
<p style="font-size:.8rem;color:var(--text-muted);margin-bottom:.5rem">单张明信片展示控制(关闭展示后可单独隐藏)</p>
<p style="font-size:.8rem;color:var(--text-muted);margin-bottom:.5rem">{{ 'profile_showcase.single_control'|t(user.language) }}</p>
{% for pc in pc_list %}
<form method="post" action="/postcards/{{ pc.id }}/showcase-toggle" style="display:inline-flex;align-items:center;gap:.3rem;margin-right:.75rem;margin-bottom:.3rem;font-size:.8rem">
<span>{{ pc.card_number }}</span>
@@ -62,7 +62,7 @@
<input type="checkbox" name="showcase_hidden" value="1" {% if pc.showcase_hidden %}checked{% endif %} onchange="this.form.submit()">
<span class="toggle-slider"></span>
</label>
{% if pc.showcase_hidden %}<span style="color:var(--text-muted);font-size:.75rem">隐藏</span>{% endif %}
{% if pc.showcase_hidden %}<span style="color:var(--text-muted);font-size:.75rem">{{ 'profile_showcase.hidden'|t(user.language) }}</span>{% endif %}
</form>
{% endfor %}
</div>
@@ -71,7 +71,7 @@
{% endfor %}
{% else %}
<div class="empty-state">
<p>暂无 Profile,请先创建</p>
<p>{{ 'profiles.new_ph'|t(user.language) }}</p>
</div>
{% endif %}
</div>