feat(i18n): add English localization with per-user language preference and navbar toggle
This commit is contained in:
212
app/i18n.py
Normal file
212
app/i18n.py
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# Translation keys for zh (Chinese) and en (English)
|
||||||
|
_translations: dict[str, dict[str, str]] = {
|
||||||
|
# ── Brand / Nav ──
|
||||||
|
"brand": {"zh": "📮 星笺", "en": "📮 Mailova"},
|
||||||
|
"nav.profiles": {"zh": "Profile", "en": "Profiles"},
|
||||||
|
"nav.settings": {"zh": "设置", "en": "Settings"},
|
||||||
|
"nav.invites": {"zh": "邀请码管理", "en": "Invites"},
|
||||||
|
"nav.logout": {"zh": "退出", "en": "Logout"},
|
||||||
|
|
||||||
|
# ── Auth ──
|
||||||
|
"login.title": {"zh": "登录", "en": "Login"},
|
||||||
|
"login.username": {"zh": "用户名", "en": "Username"},
|
||||||
|
"login.password": {"zh": "密码", "en": "Password"},
|
||||||
|
"login.remember": {"zh": "记住我", "en": "Remember me"},
|
||||||
|
"login.no_account": {"zh": "没有账号?", "en": "No account?"},
|
||||||
|
"login.go_register": {"zh": "去注册", "en": "Sign up"},
|
||||||
|
"register.title": {"zh": "注册", "en": "Register"},
|
||||||
|
"register.hint": {"zh": "邀请码由已有用户或管理员提供", "en": "Invite code provided by an existing user or admin"},
|
||||||
|
"register.invite_code": {"zh": "邀请码", "en": "Invite code"},
|
||||||
|
"register.invite_ph": {"zh": "8位字母数字", "en": "8-char code"},
|
||||||
|
"register.username": {"zh": "用户名", "en": "Username"},
|
||||||
|
"register.password": {"zh": "密码", "en": "Password"},
|
||||||
|
"register.confirm_password": {"zh": "确认密码", "en": "Confirm password"},
|
||||||
|
"register.has_account": {"zh": "已有账号?", "en": "Already have an account?"},
|
||||||
|
"register.go_login": {"zh": "去登录", "en": "Log in"},
|
||||||
|
|
||||||
|
# ── Dashboard ──
|
||||||
|
"dash.welcome": {"zh": "👋 欢迎,", "en": "👋 Welcome, "},
|
||||||
|
"dash.total_cards": {"zh": "全部明信片", "en": "Total postcards"},
|
||||||
|
"dash.sent": {"zh": "已寄出", "en": "Sent"},
|
||||||
|
"dash.delivered": {"zh": "已送达", "en": "Delivered"},
|
||||||
|
"dash.received": {"zh": "已收到", "en": "Received"},
|
||||||
|
"dash.countries": {"zh": "国家/地区", "en": "Countries"},
|
||||||
|
"dash.chart_sent": {"zh": "📤 寄出", "en": "📤 Sent"},
|
||||||
|
"dash.chart_received": {"zh": "📬 收到", "en": "📬 Received"},
|
||||||
|
|
||||||
|
# ── Profiles ──
|
||||||
|
"profiles.title": {"zh": "Profile", "en": "Profiles"},
|
||||||
|
"profiles.new_ph": {"zh": "新 Profile 昵称", "en": "New profile nickname"},
|
||||||
|
"profiles.new_btn": {"zh": "+ 新建", "en": "+ New"},
|
||||||
|
"profiles.rename": {"zh": "改名", "en": "Rename"},
|
||||||
|
"profiles.total": {"zh": "总计", "en": "Total"},
|
||||||
|
"profiles.countries": {"zh": "国家", "en": "Countries"},
|
||||||
|
"profiles.notes_ph": {"zh": "备注(仅自己可见)", "en": "Notes (private)"},
|
||||||
|
"profiles.save": {"zh": "保存", "en": "Save"},
|
||||||
|
"profiles.created_at": {"zh": "创建于", "en": "Created"},
|
||||||
|
"profiles.view": {"zh": "查看", "en": "View"},
|
||||||
|
"profiles.delete": {"zh": "删除", "en": "Delete"},
|
||||||
|
"profiles.confirm_delete": {"zh": "确定删除此 Profile?", "en": "Delete this profile?"},
|
||||||
|
|
||||||
|
# ── Postcards list ──
|
||||||
|
"pc_list.title": {"zh": " 的明信片", "en": "'s postcards"},
|
||||||
|
"pc_list.new": {"zh": "+ 新明信片", "en": "+ New postcard"},
|
||||||
|
"pc_list.all_countries": {"zh": "全部国家", "en": "All countries"},
|
||||||
|
"pc_list.filter": {"zh": "筛选", "en": "Filter"},
|
||||||
|
"pc_list.tab_sent": {"zh": "寄出", "en": "Sent"},
|
||||||
|
"pc_list.tab_received": {"zh": "收到", "en": "Received"},
|
||||||
|
"pc_list.mark_delivered": {"zh": "已送达", "en": "Delivered"},
|
||||||
|
"pc_list.confirm_delivered": {"zh": "确认已送达?", "en": "Mark as delivered?"},
|
||||||
|
"pc_list.empty_sent": {"zh": "没有寄出的明信片。", "en": "No sent postcards."},
|
||||||
|
"pc_list.empty_received": {"zh": "没有收到的明信片。", "en": "No received postcards."},
|
||||||
|
|
||||||
|
# ── Postcard detail ──
|
||||||
|
"pc_detail.title": {"zh": "明信片详情", "en": "Postcard detail"},
|
||||||
|
"pc_detail.edit": {"zh": "编辑", "en": "Edit"},
|
||||||
|
"pc_detail.mark_delivered": {"zh": "已送达", "en": "Mark delivered"},
|
||||||
|
"pc_detail.confirm_delivered": {"zh": "确认已送达?", "en": "Mark as delivered?"},
|
||||||
|
"pc_detail.delete": {"zh": "删除", "en": "Delete"},
|
||||||
|
"pc_detail.confirm_delete": {"zh": "确定删除?", "en": "Delete this postcard?"},
|
||||||
|
"pc_detail.front": {"zh": "正面", "en": "Front"},
|
||||||
|
"pc_detail.back": {"zh": "反面", "en": "Back"},
|
||||||
|
"pc_detail.no_front": {"zh": "📷 未上传正面图片", "en": "📷 No front image"},
|
||||||
|
"pc_detail.no_back": {"zh": "📷 未上传反面图片", "en": "📷 No back image"},
|
||||||
|
"pc_detail.upload_front": {"zh": "上传正面", "en": "Upload front"},
|
||||||
|
"pc_detail.upload_back": {"zh": "上传反面", "en": "Upload back"},
|
||||||
|
"pc_detail.card_number": {"zh": "编号", "en": "Number"},
|
||||||
|
"pc_detail.status": {"zh": "状态", "en": "Status"},
|
||||||
|
"pc_detail.recipient": {"zh": "收件人", "en": "Recipient"},
|
||||||
|
"pc_detail.country": {"zh": "国家/地区", "en": "Country"},
|
||||||
|
"pc_detail.send_time": {"zh": "寄出时间", "en": "Sent date"},
|
||||||
|
"pc_detail.arrival_time": {"zh": "到达时间", "en": "Arrival date"},
|
||||||
|
"pc_detail.sender": {"zh": "发件人", "en": "Sender"},
|
||||||
|
"pc_detail.receive_time": {"zh": "收到时间", "en": "Received date"},
|
||||||
|
"pc_detail.profile": {"zh": "所属 Profile", "en": "Profile"},
|
||||||
|
"pc_detail.notes": {"zh": "备注", "en": "Notes"},
|
||||||
|
|
||||||
|
# ── Status labels ──
|
||||||
|
"status.sent": {"zh": "已寄出", "en": "Sent"},
|
||||||
|
"status.delivered": {"zh": "已送达", "en": "Delivered"},
|
||||||
|
"status.received": {"zh": "已收到", "en": "Received"},
|
||||||
|
|
||||||
|
# ── Postcard form ──
|
||||||
|
"pc_form.edit": {"zh": "编辑明信片", "en": "Edit postcard"},
|
||||||
|
"pc_form.new": {"zh": "新建明信片", "en": "New postcard"},
|
||||||
|
"pc_form.number": {"zh": "编号", "en": "Number"},
|
||||||
|
"pc_form.number_hint": {"zh": "格式:AB-1234567", "en": "Format: AB-1234567"},
|
||||||
|
"pc_form.status": {"zh": "状态", "en": "Status"},
|
||||||
|
"pc_form.recipient": {"zh": "收件人", "en": "Recipient"},
|
||||||
|
"pc_form.country": {"zh": "国家/地区", "en": "Country"},
|
||||||
|
"pc_form.send_time": {"zh": "寄出时间", "en": "Sent date"},
|
||||||
|
"pc_form.arrival_time": {"zh": "到达时间", "en": "Arrival date"},
|
||||||
|
"pc_form.sender": {"zh": "发件人", "en": "Sender"},
|
||||||
|
"pc_form.receive_time": {"zh": "收到时间", "en": "Received date"},
|
||||||
|
"pc_form.notes": {"zh": "备注", "en": "Notes"},
|
||||||
|
"pc_form.notes_ph": {"zh": "可选备注信息", "en": "Optional notes"},
|
||||||
|
"pc_form.save": {"zh": "保存", "en": "Save"},
|
||||||
|
"pc_form.cancel": {"zh": "取消", "en": "Cancel"},
|
||||||
|
|
||||||
|
# ── Settings ──
|
||||||
|
"settings.title": {"zh": "用户设置", "en": "Settings"},
|
||||||
|
"settings.tab_account": {"zh": "账号", "en": "Account"},
|
||||||
|
"settings.tab_showcase": {"zh": "展示", "en": "Showcase"},
|
||||||
|
"settings.change_username": {"zh": "修改用户名", "en": "Change username"},
|
||||||
|
"settings.current_username": {"zh": "当前用户名", "en": "Current username"},
|
||||||
|
"settings.new_username": {"zh": "新用户名", "en": "New username"},
|
||||||
|
"settings.change_password": {"zh": "修改密码", "en": "Change password"},
|
||||||
|
"settings.current_password": {"zh": "当前密码", "en": "Current password"},
|
||||||
|
"settings.new_password": {"zh": "新密码", "en": "New password"},
|
||||||
|
"settings.confirm_password": {"zh": "确认新密码", "en": "Confirm new password"},
|
||||||
|
"settings.language": {"zh": "语言", "en": "Language"},
|
||||||
|
"settings.language_zh": {"zh": "中文", "en": "中文"},
|
||||||
|
"settings.language_en": {"zh": "English", "en": "English"},
|
||||||
|
"settings.showcase_global": {"zh": "全局展示设置", "en": "Global showcase settings"},
|
||||||
|
"settings.showcase_mode": {"zh": "展示页面模式", "en": "Showcase mode"},
|
||||||
|
"settings.mode_profile": {"zh": "按 Profile 分类", "en": "Grouped by profile"},
|
||||||
|
"settings.mode_flat": {"zh": "全部合并", "en": "All together"},
|
||||||
|
"settings.save_global": {"zh": "保存全局设置", "en": "Save global settings"},
|
||||||
|
"settings.public_url": {"zh": "公开展示页地址:", "en": "Public showcase: "},
|
||||||
|
"settings.public_url_label": {"zh": "公开展示页:", "en": "Public showcase: "},
|
||||||
|
"settings.profiles_showcase": {"zh": "各 Profile 展示设置", "en": "Per-profile showcase"},
|
||||||
|
"settings.showcase_on": {"zh": "展示中", "en": "Showing"},
|
||||||
|
"settings.showcase_off": {"zh": "未开启", "en": "Off"},
|
||||||
|
"settings.enable_showcase": {"zh": "开启展示", "en": "Enable showcase"},
|
||||||
|
"settings.showcase_content": {"zh": "展示内容", "en": "Content"},
|
||||||
|
"settings.content_both": {"zh": "发出 + 收到", "en": "Sent + Received"},
|
||||||
|
"settings.content_sent": {"zh": "仅发出", "en": "Sent only"},
|
||||||
|
"settings.content_received": {"zh": "仅收到", "en": "Received only"},
|
||||||
|
"settings.save": {"zh": "保存", "en": "Save"},
|
||||||
|
|
||||||
|
# ── Admin invites ──
|
||||||
|
"invites.title": {"zh": "邀请码管理", "en": "Invite codes"},
|
||||||
|
"invites.create": {"zh": "创建邀请码", "en": "Create invite code"},
|
||||||
|
"invites.code_label": {"zh": "邀请码(留空自动生成)", "en": "Code (leave empty to auto-generate)"},
|
||||||
|
"invites.code_ph": {"zh": "自动生成", "en": "Auto-generate"},
|
||||||
|
"invites.limit_type": {"zh": "限制类型", "en": "Limit type"},
|
||||||
|
"invites.unlimited": {"zh": "无限制", "en": "Unlimited"},
|
||||||
|
"invites.uses": {"zh": "使用次数", "en": "Max uses"},
|
||||||
|
"invites.expires": {"zh": "有效期", "en": "Expiration"},
|
||||||
|
"invites.times": {"zh": "次数", "en": "Times"},
|
||||||
|
"invites.create_btn": {"zh": "创建", "en": "Create"},
|
||||||
|
"invites.creator": {"zh": "创建者", "en": "By"},
|
||||||
|
"invites.used": {"zh": "已用", "en": "Used"},
|
||||||
|
"invites.no_expire": {"zh": "永不过期", "en": "No expiration"},
|
||||||
|
"invites.expires_at": {"zh": "过期", "en": "Expires"},
|
||||||
|
"invites.edit": {"zh": "编辑", "en": "Edit"},
|
||||||
|
"invites.save": {"zh": "保存", "en": "Save"},
|
||||||
|
"invites.delete": {"zh": "删除", "en": "Delete"},
|
||||||
|
"invites.confirm_delete": {"zh": "确定删除此邀请码?", "en": "Delete this invite code?"},
|
||||||
|
"invites.max_uses": {"zh": "最大使用次数", "en": "Max uses"},
|
||||||
|
"invites.expires_time": {"zh": "过期时间", "en": "Expiration"},
|
||||||
|
"invites.registered_users": {"zh": "注册用户", "en": "Registered users"},
|
||||||
|
|
||||||
|
# ── Showcase (public) ──
|
||||||
|
"showcase.title": {"zh": " 的明信片", "en": "'s postcards"},
|
||||||
|
"showcase.empty": {"zh": "暂无展示内容", "en": "Nothing to show yet"},
|
||||||
|
"showcase.tab_sent": {"zh": "发出", "en": "Sent"},
|
||||||
|
"showcase.tab_received": {"zh": "收到", "en": "Received"},
|
||||||
|
|
||||||
|
# ── Showcase manage ──
|
||||||
|
"showcase_manage.title": {"zh": "展示管理", "en": "Showcase manager"},
|
||||||
|
"showcase_manage.global": {"zh": "全局展示设置", "en": "Global showcase settings"},
|
||||||
|
"showcase_manage.save": {"zh": "保存全局设置", "en": "Save global settings"},
|
||||||
|
"showcase_manage.public_url": {"zh": "公开展示页地址:", "en": "Public showcase: "},
|
||||||
|
"showcase_manage.profiles": {"zh": "各 Profile 展示设置", "en": "Per-profile settings"},
|
||||||
|
"showcase_manage.on": {"zh": "展示中", "en": "Showing"},
|
||||||
|
"showcase_manage.off": {"zh": "未开启", "en": "Off"},
|
||||||
|
"showcase_manage.enable": {"zh": "开启展示", "en": "Enable"},
|
||||||
|
"showcase_manage.content": {"zh": "展示内容", "en": "Content"},
|
||||||
|
"showcase_manage.both": {"zh": "发出 + 收到", "en": "Sent + Received"},
|
||||||
|
"showcase_manage.sent_only": {"zh": "仅发出", "en": "Sent only"},
|
||||||
|
"showcase_manage.received_only": {"zh": "仅收到", "en": "Received only"},
|
||||||
|
|
||||||
|
# ── Profile showcase ──
|
||||||
|
"profile_showcase.title": {"zh": " 展示设置", "en": " showcase settings"},
|
||||||
|
"profile_showcase.enable": {"zh": "开启展示", "en": "Enable showcase"},
|
||||||
|
"profile_showcase.content": {"zh": "展示内容", "en": "Content"},
|
||||||
|
"profile_showcase.both": {"zh": "发出 + 收到", "en": "Sent + Received"},
|
||||||
|
"profile_showcase.sent_only": {"zh": "仅发出", "en": "Sent only"},
|
||||||
|
"profile_showcase.received_only": {"zh": "仅收到", "en": "Received only"},
|
||||||
|
"profile_showcase.single_control": {"zh": "单张明信片展示控制", "en": "Per-card visibility"},
|
||||||
|
"profile_showcase.hidden": {"zh": "隐藏", "en": "Hidden"},
|
||||||
|
"profile_showcase.hint": {"zh": "开启展示后可控制单张明信片的可见性", "en": "Enable showcase to control per-card visibility"},
|
||||||
|
|
||||||
|
# ── Flash messages ──
|
||||||
|
"flash.username_taken": {"zh": "用户名已存在", "en": "Username already taken"},
|
||||||
|
"flash.username_ok": {"zh": "用户名修改成功", "en": "Username changed"},
|
||||||
|
"flash.wrong_password": {"zh": "当前密码错误", "en": "Wrong password"},
|
||||||
|
"flash.password_mismatch": {"zh": "两次输入的新密码不一致", "en": "Passwords don't match"},
|
||||||
|
"flash.password_short": {"zh": "新密码长度至少6位", "en": "Password must be at least 6 characters"},
|
||||||
|
"flash.password_ok": {"zh": "密码修改成功", "en": "Password changed"},
|
||||||
|
|
||||||
|
# ── Misc ──
|
||||||
|
"misc.required": {"zh": " *", "en": " *"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def t(key: str, lang: str = "zh") -> str:
|
||||||
|
"""Translate a key to the given language. Falls back to zh, then key itself."""
|
||||||
|
entry = _translations.get(key)
|
||||||
|
if entry is None:
|
||||||
|
return key
|
||||||
|
return entry.get(lang) or entry.get("zh") or key
|
||||||
@@ -19,6 +19,7 @@ class User(Base):
|
|||||||
is_admin = Column(Boolean, nullable=False, default=False)
|
is_admin = Column(Boolean, nullable=False, default=False)
|
||||||
invited_by_code_id = Column(Integer, ForeignKey("invite_codes.id", ondelete="SET NULL"), nullable=True)
|
invited_by_code_id = Column(Integer, ForeignKey("invite_codes.id", ondelete="SET NULL"), nullable=True)
|
||||||
showcase_mode = Column(String(16), nullable=False, default="profile")
|
showcase_mode = Column(String(16), nullable=False, default="profile")
|
||||||
|
language = Column(String(4), nullable=False, default="zh")
|
||||||
created_at = Column(DateTime, default=_utcnow)
|
created_at = Column(DateTime, default=_utcnow)
|
||||||
|
|
||||||
profiles = relationship("Profile", back_populates="user", cascade="all, delete-orphan")
|
profiles = relationship("Profile", back_populates="user", cascade="all, delete-orphan")
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from random import choice
|
|||||||
from string import ascii_letters, digits
|
from string import ascii_letters, digits
|
||||||
|
|
||||||
from app.models import InviteCode, Postcard, Profile, User
|
from app.models import InviteCode, Postcard, Profile, User
|
||||||
|
from app.i18n import t as _t
|
||||||
|
|
||||||
router = APIRouter(tags=["web"])
|
router = APIRouter(tags=["web"])
|
||||||
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent.parent / "templates"))
|
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent.parent / "templates"))
|
||||||
@@ -66,6 +67,13 @@ def _strftime(dt: datetime, fmt: str) -> str:
|
|||||||
templates.env.filters["strftime"] = _strftime
|
templates.env.filters["strftime"] = _strftime
|
||||||
|
|
||||||
|
|
||||||
|
def _translate_filter(key: str, lang: str = "zh") -> str:
|
||||||
|
return _t(key, lang)
|
||||||
|
|
||||||
|
|
||||||
|
templates.env.filters["t"] = _translate_filter
|
||||||
|
|
||||||
|
|
||||||
def _redirect(url: str) -> RedirectResponse:
|
def _redirect(url: str) -> RedirectResponse:
|
||||||
return RedirectResponse(url, status_code=303)
|
return RedirectResponse(url, status_code=303)
|
||||||
|
|
||||||
@@ -353,6 +361,19 @@ def change_password(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/settings/language")
|
||||||
|
def change_language(
|
||||||
|
request: Request,
|
||||||
|
language: str = Form(...),
|
||||||
|
user: User = Depends(get_current_user_web),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
if language in ("zh", "en"):
|
||||||
|
user.language = language
|
||||||
|
db.commit()
|
||||||
|
return _redirect(request.headers.get("referer", "/settings"))
|
||||||
|
|
||||||
|
|
||||||
# ---------- Auth ----------
|
# ---------- Auth ----------
|
||||||
|
|
||||||
@router.get("/logout")
|
@router.get("/logout")
|
||||||
@@ -798,4 +819,5 @@ def public_showcase(request: Request, username: str, db: Session = Depends(get_d
|
|||||||
return templates.TemplateResponse("showcase.html", {
|
return templates.TemplateResponse("showcase.html", {
|
||||||
"request": request, "profile_user": user, "mode": user.showcase_mode,
|
"request": request, "profile_user": user, "mode": user.showcase_mode,
|
||||||
"profile_groups": profile_groups, "sent_all": sent_all, "received_all": received_all,
|
"profile_groups": profile_groups, "sent_all": sent_all, "received_all": received_all,
|
||||||
|
"lang": user.language,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ a:hover { text-decoration: underline; }
|
|||||||
.nav-links a { color: var(--text-muted); font-size: .8rem; }
|
.nav-links a { color: var(--text-muted); font-size: .8rem; }
|
||||||
.nav-links a:hover { color: var(--primary); text-decoration: none; }
|
.nav-links a:hover { color: var(--primary); text-decoration: none; }
|
||||||
.nav-logout { color: var(--danger) !important; }
|
.nav-logout { color: var(--danger) !important; }
|
||||||
|
.nav-lang-btn { background: none; border: 1px solid var(--border); border-radius: 4px; padding: .15rem .45rem; font-size: .72rem; color: var(--text-muted); cursor: pointer; line-height: 1; }
|
||||||
|
.nav-lang-btn:hover { background: var(--bg); color: var(--primary); }
|
||||||
|
|
||||||
/* Container */
|
/* Container */
|
||||||
.container { max-width: 960px; margin: 2rem auto; padding: 0 1.5rem; }
|
.container { max-width: 960px; margin: 2rem auto; padding: 0 1.5rem; }
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}邀请码管理 - 星笺{% endblock %}
|
{% block title %}{{ 'invites.title'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h1>邀请码管理</h1>
|
<h1>{{ 'invites.title'|t(user.language) }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if success %}
|
{% if success %}
|
||||||
@@ -14,30 +14,30 @@
|
|||||||
|
|
||||||
<!-- Add new code -->
|
<!-- Add new code -->
|
||||||
<div class="form-card" style="margin-bottom:1.5rem">
|
<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">
|
<form method="post" action="/admin/invites/add" style="display:flex;flex-direction:column;gap:.85rem">
|
||||||
<div>
|
<div>
|
||||||
<label style="margin-top:0">邀请码(留空自动生成)</label>
|
<label style="margin-top:0">{{ 'invites.code_label'|t(user.language) }}</label>
|
||||||
<input type="text" name="code" maxlength="8" placeholder="自动生成" style="text-transform:uppercase">
|
<input type="text" name="code" maxlength="8" placeholder="{{ 'invites.code_ph'|t(user.language) }}" style="text-transform:uppercase">
|
||||||
</div>
|
</div>
|
||||||
<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()">
|
<select name="limit_type" id="limit_type" onchange="toggleLimitFields()">
|
||||||
<option value="unlimited">无限制</option>
|
<option value="unlimited">{{ 'invites.unlimited'|t(user.language) }}</option>
|
||||||
<option value="uses">使用次数</option>
|
<option value="uses">{{ 'invites.uses'|t(user.language) }}</option>
|
||||||
<option value="expires">有效期</option>
|
<option value="expires">{{ 'invites.expires'|t(user.language) }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div id="max_uses_field" style="display:none;align-items:center;gap:.3rem">
|
<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">
|
<input type="number" name="max_uses" min="1" value="1" class="aif-input aif-short">
|
||||||
</div>
|
</div>
|
||||||
<div id="expires_field" style="display:none;align-items:center;gap:.3rem">
|
<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">
|
<input type="datetime-local" name="expires_at" class="aif-input aif-date">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,41 +49,41 @@
|
|||||||
<div class="postcard-row" style="flex-direction:column;align-items:stretch;gap:.75rem">
|
<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">
|
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
||||||
<strong style="font-family:monospace;font-size:1.1rem">{{ c.code }}</strong>
|
<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>
|
<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 %}
|
{% 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 %}
|
{% else %}
|
||||||
<span class="badge">已用 {{ c.use_count }} / ∞</span>
|
<span class="badge">{{ 'invites.used'|t(user.language) }} {{ c.use_count }} / ∞</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if c.expires_at %}
|
{% 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 %}
|
{% else %}
|
||||||
<span class="badge">永不过期</span>
|
<span class="badge">{{ 'invites.no_expire'|t(user.language) }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if c.registered_users %}
|
{% 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 %}
|
{% endif %}
|
||||||
<form method="post" action="/admin/invites/{{ c.id }}/edit" class="admin-invite-form" id="edit-form-{{ c.id }}">
|
<form method="post" action="/admin/invites/{{ c.id }}/edit" class="admin-invite-form" id="edit-form-{{ c.id }}">
|
||||||
<div class="aif-field">
|
<div class="aif-field">
|
||||||
<select name="limit_type" class="aif-select" onchange="toggleEditFields({{ c.id }}, this.value)">
|
<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="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 %}>次数限制</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 %}>有效期</option>
|
<option value="expires"{% if c.expires_at %} selected{% endif %}>{{ 'invites.expires'|t(user.language) }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<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">
|
<input type="number" name="max_uses" value="{{ c.max_uses or 1 }}" min="1" class="aif-input aif-short">
|
||||||
</div>
|
</div>
|
||||||
<div style="display:{% if c.expires_at %}inline-flex{% else %}none{% endif %};align-items:center;gap:.3rem;align-self:flex-end">
|
<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">
|
<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>
|
||||||
<div style="display:flex;gap:.4rem;align-self:flex-end">
|
<div style="display:flex;gap:.4rem;align-self:flex-end">
|
||||||
<button type="submit" class="btn btn-sm">保存</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('确定删除?'))this.closest('form').nextElementSibling.submit()">删除</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="/admin/invites/{{ c.id }}/delete" style="display:none"></form>
|
<form method="post" action="/admin/invites/{{ c.id }}/delete" style="display:none"></form>
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-icon">🔑</div>
|
<div class="empty-icon">🔑</div>
|
||||||
<p>暂无邀请码</p>
|
<p>{{ 'invites.title'|t(user.language) }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,19 @@
|
|||||||
<body>
|
<body>
|
||||||
{% block nav %}
|
{% block nav %}
|
||||||
<nav class="navbar">
|
<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">
|
<div class="nav-links">
|
||||||
{% if user is defined and user %}
|
{% if user is defined and user %}
|
||||||
<a href="/profiles">Profile</a>
|
<a href="/profiles">{{ 'nav.profiles'|t(user.language) }}</a>
|
||||||
<a href="/settings">设置</a>
|
<a href="/settings">{{ 'nav.settings'|t(user.language) }}</a>
|
||||||
{% if user.is_admin %}
|
{% if user.is_admin %}
|
||||||
<a href="/admin/invites">邀请码管理</a>
|
<a href="/admin/invites">{{ 'nav.invites'|t(user.language) }}</a>
|
||||||
{% endif %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}总览 - 星笺{% endblock %}
|
{% block title %}{{ {'zh':'总览','en':'Dashboard'}[user.language] }} - {{ 'brand'|t(user.language) }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h1>👋 欢迎,{{ user.username }}</h1>
|
<h1>{{ 'dash.welcome'|t(user.language) }}{{ user.username }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if profiles %}
|
{% if profiles %}
|
||||||
@@ -11,35 +11,35 @@
|
|||||||
<div class="dash-stat-icon">📮</div>
|
<div class="dash-stat-icon">📮</div>
|
||||||
<div class="dash-stat-body">
|
<div class="dash-stat-body">
|
||||||
<span class="dash-stat-value">{{ total }}</span>
|
<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>
|
</div>
|
||||||
<div class="dash-stat-card">
|
<div class="dash-stat-card">
|
||||||
<div class="dash-stat-icon">📤</div>
|
<div class="dash-stat-icon">📤</div>
|
||||||
<div class="dash-stat-body">
|
<div class="dash-stat-body">
|
||||||
<span class="dash-stat-value dash-sent">{{ sent }}</span>
|
<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>
|
</div>
|
||||||
<div class="dash-stat-card">
|
<div class="dash-stat-card">
|
||||||
<div class="dash-stat-icon">📬</div>
|
<div class="dash-stat-icon">📬</div>
|
||||||
<div class="dash-stat-body">
|
<div class="dash-stat-body">
|
||||||
<span class="dash-stat-value dash-delivered">{{ delivered }}</span>
|
<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>
|
</div>
|
||||||
<div class="dash-stat-card">
|
<div class="dash-stat-card">
|
||||||
<div class="dash-stat-icon">💌</div>
|
<div class="dash-stat-icon">💌</div>
|
||||||
<div class="dash-stat-body">
|
<div class="dash-stat-body">
|
||||||
<span class="dash-stat-value dash-received">{{ received }}</span>
|
<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>
|
</div>
|
||||||
<div class="dash-stat-card">
|
<div class="dash-stat-card">
|
||||||
<div class="dash-stat-icon">🌍</div>
|
<div class="dash-stat-icon">🌍</div>
|
||||||
<div class="dash-stat-body">
|
<div class="dash-stat-body">
|
||||||
<span class="dash-stat-value">{{ countries }}</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,9 +52,9 @@
|
|||||||
<div class="progress-seg progress-received" style="width:{{ received_pct }}%"></div>
|
<div class="progress-seg progress-received" style="width:{{ received_pct }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-legend">
|
<div class="progress-legend">
|
||||||
<span><i class="dot dot-sent"></i>已寄出 {{ sent_pct }}%</span>
|
<span><i class="dot dot-sent"></i>{{ 'dash.sent'|t(user.language) }} {{ sent_pct }}%</span>
|
||||||
<span><i class="dot dot-delivered"></i>已送达 {{ delivered_pct }}%</span>
|
<span><i class="dot dot-delivered"></i>{{ 'dash.delivered'|t(user.language) }} {{ delivered_pct }}%</span>
|
||||||
<span><i class="dot dot-received"></i>已收到 {{ received_pct }}%</span>
|
<span><i class="dot dot-received"></i>{{ 'dash.received'|t(user.language) }} {{ received_pct }}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -64,109 +64,62 @@
|
|||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1.5rem">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1.5rem">
|
||||||
{% if sent_country_stats %}
|
{% if sent_country_stats %}
|
||||||
<div style="background:var(--card-bg);border:1px solid var(--border);border-radius:10px;padding:1.25rem">
|
<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>
|
<canvas id="chart-sent"></canvas>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if received_country_stats %}
|
{% if received_country_stats %}
|
||||||
<div style="background:var(--card-bg);border:1px solid var(--border);border-radius:10px;padding:1.25rem">
|
<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>
|
<canvas id="chart-received"></canvas>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
{% if sent_country_stats %}
|
(function() {
|
||||||
new Chart(document.getElementById('chart-sent'), {
|
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',
|
type: 'bar',
|
||||||
data: {
|
data: { labels: labels, datasets: [{ data: data, backgroundColor: colors, borderRadius: 4 }] },
|
||||||
labels: [{% for code, _ in sent_country_stats %}'{{ code|flag }} {{ code }}',{% endfor %}],
|
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } }
|
||||||
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 %}
|
{% endfor %}
|
||||||
{% if received_country_stats %}
|
{% for stat in received_country_stats %}
|
||||||
new Chart(document.getElementById('chart-received'), {
|
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',
|
type: 'bar',
|
||||||
data: {
|
data: { labels: labels, datasets: [{ data: data, backgroundColor: colors, borderRadius: 4 }] },
|
||||||
labels: [{% for code, _ in received_country_stats %}'{{ code|flag }} {{ code }}',{% endfor %}],
|
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } }
|
||||||
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 %}
|
{% endfor %}
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if profiles %}
|
||||||
<div class="dash-sections">
|
<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 %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p class="empty">暂无明信片</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dash-section">
|
|
||||||
<h2>Profile</h2>
|
|
||||||
<div class="dash-profiles">
|
|
||||||
{% for p in profiles %}
|
{% for p in profiles %}
|
||||||
<a href="/profiles/{{ p.id }}/postcards" class="dash-profile-link">
|
<a href="/profiles/{{ p.id }}/postcards" class="card dashboard-card">
|
||||||
<span class="dash-profile-name">{{ p.nickname }}</span>
|
<h3>{{ p.nickname }}</h3>
|
||||||
<span class="dash-profile-arrow">→</span>
|
<div class="stats">
|
||||||
|
<span class="stat">{{ p.postcards|length }} {{ 'profiles.total'|t(user.language) }}</span>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-icon">📮</div>
|
<p>{{ 'dash.welcome'|t(user.language) }}{{ user.username }} 👋</p>
|
||||||
<p>欢迎使用星笺!</p>
|
|
||||||
<p class="empty-sub">创建一个 <a href="/profiles">Profile</a> 开始管理你的明信片</p>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block content %}
|
||||||
<div class="auth-form">
|
<div class="auth-form">
|
||||||
<h2>登录</h2>
|
<h2>{{ 'login.title'|t(user.language if user else 'zh') }}</h2>
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="alert alert-error">{{ error }}</div>
|
<div class="alert alert-error">{{ error }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post" action="/login">
|
<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>
|
<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>
|
<input type="password" name="password" required>
|
||||||
<label class="checkbox-label" style="margin-top:1rem;display:flex;align-items:center;gap:.4rem;cursor:pointer">
|
<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>
|
</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>
|
</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>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,67 +1,67 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}明信片详情 - 星笺{% endblock %}
|
{% block title %}{{ 'pc_detail.title'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div class="detail-title-row">
|
<div class="detail-title-row">
|
||||||
<a href="/profiles/{{ postcard.profile_id }}/postcards" class="btn btn-back">←</a>
|
<a href="/profiles/{{ postcard.profile_id }}/postcards" class="btn btn-back">←</a>
|
||||||
<h1>{{ postcard.card_number }}</h1>
|
<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>
|
||||||
<div class="detail-actions">
|
<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' %}
|
{% if postcard.status == 'sent' %}
|
||||||
<form method="post" action="/postcards/{{ postcard.id }}/mark-delivered" class="inline">
|
<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>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post" action="/postcards/{{ postcard.id }}/delete" class="inline">
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<div class="detail-images">
|
<div class="detail-images">
|
||||||
<div class="image-block">
|
<div class="image-block">
|
||||||
<h3>正面</h3>
|
<h3>{{ 'pc_detail.front'|t(user.language) }}</h3>
|
||||||
{% if postcard.image_front %}
|
{% if postcard.image_front %}
|
||||||
<img src="{{ postcard.image_front }}" alt="正面" class="detail-img">
|
<img src="{{ postcard.image_front }}" alt="" class="detail-img">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="detail-img placeholder">📷 未上传正面图片</div>
|
<div class="detail-img placeholder">{{ 'pc_detail.no_front'|t(user.language) }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post" action="/postcards/{{ postcard.id }}/image/front" enctype="multipart/form-data" class="inline upload-form">
|
<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>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="image-block">
|
<div class="image-block">
|
||||||
<h3>反面</h3>
|
<h3>{{ 'pc_detail.back'|t(user.language) }}</h3>
|
||||||
{% if postcard.image_back %}
|
{% if postcard.image_back %}
|
||||||
<img src="{{ postcard.image_back }}" alt="反面" class="detail-img">
|
<img src="{{ postcard.image_back }}" alt="" class="detail-img">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="detail-img placeholder">📷 未上传反面图片</div>
|
<div class="detail-img placeholder">{{ 'pc_detail.no_back'|t(user.language) }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post" action="/postcards/{{ postcard.id }}/image/back" enctype="multipart/form-data" class="inline upload-form">
|
<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>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-meta">
|
<div class="detail-meta">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>编号</dt><dd>{{ postcard.card_number }}</dd>
|
<dt>{{ 'pc_detail.card_number'|t(user.language) }}</dt><dd>{{ postcard.card_number }}</dd>
|
||||||
<dt>状态</dt><dd>{{ {'sent':'已寄出','delivered':'已送达','received':'已收到'}.get(postcard.status, postcard.status) }}</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') %}
|
{% if postcard.status in ('sent','delivered') %}
|
||||||
<dt>收件人</dt><dd>{{ postcard.recipient_name or '-' }}</dd>
|
<dt>{{ 'pc_detail.recipient'|t(user.language) }}</dt><dd>{{ postcard.recipient_name or '-' }}</dd>
|
||||||
<dt>国家/地区</dt><dd>{{ postcard.country|flag }} {{ postcard.country or '-' }}</dd>
|
<dt>{{ 'pc_detail.country'|t(user.language) }}</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>{{ 'pc_detail.send_time'|t(user.language) }}</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.arrival_time'|t(user.language) }}</dt><dd>{{ postcard.arrival_time.strftime('%Y-%m-%d') if postcard.arrival_time else '-' }}</dd>
|
||||||
{% else %}
|
{% else %}
|
||||||
<dt>发件人</dt><dd>{{ postcard.sender_name or '-' }}</dd>
|
<dt>{{ 'pc_detail.sender'|t(user.language) }}</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.receive_time'|t(user.language) }}</dt><dd>{{ postcard.receive_time.strftime('%Y-%m-%d') if postcard.receive_time else '-' }}</dd>
|
||||||
{% endif %}
|
{% 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 %}
|
{% if postcard.notes %}
|
||||||
<dt>备注</dt><dd>{{ postcard.notes }}</dd>
|
<dt>{{ 'pc_detail.notes'|t(user.language) }}</dt><dd>{{ postcard.notes }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block content %}
|
||||||
<div class="section-header">
|
<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>
|
</div>
|
||||||
<form method="post" action="{{ '/postcards/%d/edit' % postcard.id if postcard else '/profiles/%d/postcards' % profile.id }}" class="form-card">
|
<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()">
|
<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>
|
<select name="status" id="status-select" onchange="toggleFields()" required>
|
||||||
<option value="sent" {% if postcard and postcard.status=='sent' %}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 %}>已送达</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 %}>已收到</option>
|
<option value="received" {% if postcard and postcard.status=='received' %}selected{% endif %}>{{ 'status.received'|t(user.language) }}</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div id="fields-sent-delivered">
|
<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 '' }}">
|
<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()">
|
<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 '' }}">
|
<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 '' }}">
|
<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>
|
||||||
|
|
||||||
<div id="fields-received" style="display:none">
|
<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 '' }}">
|
<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 '' }}">
|
<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>
|
</div>
|
||||||
|
|
||||||
<label>备注</label>
|
<label>{{ 'pc_form.notes'|t(user.language) }}</label>
|
||||||
<textarea name="notes" rows="3" placeholder="可选备注信息">{{ postcard.notes if postcard else '' }}</textarea>
|
<textarea name="notes" rows="3" placeholder="{{ 'pc_form.notes_ph'|t(user.language) }}">{{ postcard.notes if postcard else '' }}</textarea>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">保存</button>
|
<button type="submit" class="btn btn-primary">{{ 'pc_form.save'|t(user.language) }}</button>
|
||||||
<a href="/profiles/{{ profile.id }}/postcards" class="btn">取消</a>
|
<a href="/profiles/{{ profile.id }}/postcards" class="btn">{{ 'pc_form.cancel'|t(user.language) }}</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<script>
|
<script>
|
||||||
@@ -60,7 +60,6 @@ function toggleFields() {
|
|||||||
country.required = false;
|
country.required = false;
|
||||||
sendTime.required = false;
|
sendTime.required = false;
|
||||||
arrival.required = false;
|
arrival.required = false;
|
||||||
arrival.disabled = false;
|
|
||||||
sender.required = true;
|
sender.required = true;
|
||||||
receive.required = true;
|
receive.required = true;
|
||||||
} else if (s === 'sent') {
|
} else if (s === 'sent') {
|
||||||
@@ -72,7 +71,6 @@ function toggleFields() {
|
|||||||
arrival.required = false;
|
arrival.required = false;
|
||||||
arrival.disabled = true;
|
arrival.disabled = true;
|
||||||
arrival.value = '';
|
arrival.value = '';
|
||||||
labelArrival.textContent = '到达时间';
|
|
||||||
sender.required = false;
|
sender.required = false;
|
||||||
receive.required = false;
|
receive.required = false;
|
||||||
} else {
|
} else {
|
||||||
@@ -83,23 +81,19 @@ function toggleFields() {
|
|||||||
sendTime.required = true;
|
sendTime.required = true;
|
||||||
arrival.disabled = false;
|
arrival.disabled = false;
|
||||||
arrival.required = true;
|
arrival.required = true;
|
||||||
labelArrival.textContent = '到达时间 *';
|
|
||||||
sender.required = false;
|
sender.required = false;
|
||||||
receive.required = false;
|
receive.required = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toggleFields();
|
toggleFields();
|
||||||
{% if not postcard %}
|
{% if not postcard %}
|
||||||
// Auto-fill date fields with today on new postcards
|
|
||||||
(function() {
|
(function() {
|
||||||
var today = new Date().toISOString().slice(0, 10);
|
var today = new Date().toISOString().slice(0, 10);
|
||||||
var sendTime = document.getElementById('input-send-time');
|
var sendTime = document.getElementById('input-send-time');
|
||||||
var receiveTime = document.getElementById('input-receive');
|
var receiveTime = document.getElementById('input-receive');
|
||||||
var statusSel = document.getElementById('status-select');
|
var statusSel = document.getElementById('status-select');
|
||||||
// Fill on page load
|
|
||||||
if (statusSel.value === 'sent' && !sendTime.value) sendTime.value = today;
|
if (statusSel.value === 'sent' && !sendTime.value) sendTime.value = today;
|
||||||
if (statusSel.value === 'received' && !receiveTime.value) receiveTime.value = today;
|
if (statusSel.value === 'received' && !receiveTime.value) receiveTime.value = today;
|
||||||
// Fill on status change
|
|
||||||
statusSel.addEventListener('change', function() {
|
statusSel.addEventListener('change', function() {
|
||||||
if (this.value === 'sent' && !sendTime.value) sendTime.value = today;
|
if (this.value === 'sent' && !sendTime.value) sendTime.value = today;
|
||||||
if (this.value === 'received' && !receiveTime.value) receiveTime.value = today;
|
if (this.value === 'received' && !receiveTime.value) receiveTime.value = today;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{{ profile.nickname }} - 星笺{% endblock %}
|
{% block title %}{{ profile.nickname }} - {{ 'brand'|t(user.language) }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h1>{{ profile.nickname }} 的明信片</h1>
|
<h1>{{ profile.nickname }} {{ 'pc_list.title'|t(user.language) }}</h1>
|
||||||
<a href="/profiles/{{ profile.id }}/postcards/new" class="btn btn-primary">+ 新明信片</a>
|
<a href="/profiles/{{ profile.id }}/postcards/new" class="btn btn-primary">{{ 'pc_list.new'|t(user.language) }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-bottom:1rem">
|
<div style="margin-bottom:1rem">
|
||||||
{% if profile.notes %}
|
{% if profile.notes %}
|
||||||
@@ -13,18 +13,18 @@
|
|||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
<form method="get" class="form-row">
|
<form method="get" class="form-row">
|
||||||
<select name="country">
|
<select name="country">
|
||||||
<option value="">全部国家</option>
|
<option value="">{{ 'pc_list.all_countries'|t(user.language) }}</option>
|
||||||
{% for c in countries %}
|
{% for c in countries %}
|
||||||
<option value="{{ c }}" {% if country == c %}selected{% endif %}>{{ c|flag }} {{ c }}</option>
|
<option value="{{ c }}" {% if country == c %}selected{% endif %}>{{ c|flag }} {{ c }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="btn">筛选</button>
|
<button type="submit" class="btn">{{ 'pc_list.filter'|t(user.language) }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tabs" id="tabs">
|
<div class="tabs" id="tabs">
|
||||||
<button class="tab active" onclick="switchTab('sent')">寄出 ({{ sent|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')">收到 ({{ received|length }})</button>
|
<button class="tab" onclick="switchTab('received')">{{ 'pc_list.tab_received'|t(user.language) }} ({{ received|length }})</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tab-sent" class="tab-panel">
|
<div id="tab-sent" class="tab-panel">
|
||||||
@@ -33,7 +33,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="正面" loading="lazy">
|
<img src="{{ pc.image_front }}" alt="" loading="lazy">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="thumb placeholder">📷</div>
|
<div class="thumb placeholder">📷</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -44,24 +44,24 @@
|
|||||||
<span>{{ pc.country|flag }} {{ pc.country or '-' }}</span>
|
<span>{{ pc.country|flag }} {{ pc.country or '-' }}</span>
|
||||||
<span>→ {{ pc.recipient_name or '-' }}</span>
|
<span>→ {{ pc.recipient_name or '-' }}</span>
|
||||||
</div>
|
</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">
|
<span class="postcard-row-date">
|
||||||
{% if pc.send_time %}{{ pc.send_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') }}送达{% endif %}
|
{% if pc.arrival_time %}<br>{{ pc.arrival_time.strftime('%Y-%m-%d') }}{% if user.language=='zh' %}送达{% endif %}{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% if pc.notes %}<span class="postcard-row-notes">{{ pc.notes[:50] }}{% if pc.notes|length > 50 %}…{% endif %}</span>{% endif %}
|
{% if pc.notes %}<span class="postcard-row-notes">{{ pc.notes[:50] }}{% if pc.notes|length > 50 %}…{% endif %}</span>{% endif %}
|
||||||
<div class="postcard-row-action">
|
<div class="postcard-row-action">
|
||||||
{% if pc.status == 'sent' %}
|
{% if pc.status == 'sent' %}
|
||||||
<form method="post" action="/postcards/{{ pc.id }}/mark-delivered" onclick="event.preventDefault();event.stopPropagation();if(confirm('确认已送达?'))this.submit();">
|
<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">已送达</button>
|
<button type="submit" class="btn btn-sm btn-primary">{{ 'pc_list.mark_delivered'|t(user.language) }}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="empty">没有寄出的明信片。</p>
|
<p class="empty">{{ 'pc_list.empty_sent'|t(user.language) }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -71,7 +71,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="正面" loading="lazy">
|
<img src="{{ pc.image_front }}" alt="" loading="lazy">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="thumb placeholder">📷</div>
|
<div class="thumb placeholder">📷</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -81,13 +81,13 @@
|
|||||||
<div class="postcard-row-detail">
|
<div class="postcard-row-detail">
|
||||||
<span>← {{ pc.sender_name or '-' }}</span>
|
<span>← {{ pc.sender_name or '-' }}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{% if pc.notes %}<span class="postcard-row-notes">{{ pc.notes[:50] }}{% if pc.notes|length > 50 %}…{% endif %}</span>{% endif %}
|
{% if pc.notes %}<span class="postcard-row-notes">{{ pc.notes[:50] }}{% if pc.notes|length > 50 %}…{% endif %}</span>{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="empty">没有收到的明信片。</p>
|
<p class="empty">{{ 'pc_list.empty_received'|t(user.language) }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block content %}
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div class="detail-title-row">
|
<div class="detail-title-row">
|
||||||
<a href="/settings?tab=showcase" class="btn btn-back">←</a>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="form-card">
|
<div class="form-card">
|
||||||
<form method="post" action="/profiles/{{ profile.id }}/showcase" style="display:flex;flex-wrap:wrap;gap:1rem;align-items:center">
|
<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">
|
<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">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" name="showcase_enabled" value="1" {% if profile.showcase_enabled %}checked{% endif %} onchange="this.form.submit()">
|
<input type="checkbox" name="showcase_enabled" value="1" {% if profile.showcase_enabled %}checked{% endif %} onchange="this.form.submit()">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;align-items:center;gap:.5rem">
|
<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">
|
<select name="showcase_visible" onchange="this.form.submit()" style="width:auto">
|
||||||
<option value="both" {% if profile.showcase_visible == 'both' %}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 %}>仅发出</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 %}>仅收到</option>
|
<option value="received" {% if profile.showcase_visible == 'received' %}selected{% endif %}>{{ 'profile_showcase.received_only'|t(user.language) }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -30,19 +30,19 @@
|
|||||||
|
|
||||||
{% if profile.showcase_enabled and cards %}
|
{% if profile.showcase_enabled and cards %}
|
||||||
<div style="margin-top:1.5rem">
|
<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">
|
<div style="background:var(--card-bg);border:1px solid var(--border);border-radius:10px;padding:1rem">
|
||||||
{% for pc in cards %}
|
{% 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">
|
<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>{{ 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 %}">
|
<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>
|
</span>
|
||||||
<label class="toggle-switch toggle-sm">
|
<label class="toggle-switch toggle-sm">
|
||||||
<input type="checkbox" name="showcase_hidden" value="1" {% if pc.showcase_hidden %}checked{% endif %} onchange="this.form.submit()">
|
<input type="checkbox" name="showcase_hidden" value="1" {% if pc.showcase_hidden %}checked{% endif %} onchange="this.form.submit()">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</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>
|
</form>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
|
|
||||||
{% if not profile.showcase_enabled %}
|
{% if not profile.showcase_enabled %}
|
||||||
<div class="empty-state" style="margin-top:2rem">
|
<div class="empty-state" style="margin-top:2rem">
|
||||||
<p>开启展示后可控制单张明信片的可见性</p>
|
<p>{{ 'profile_showcase.hint'|t(user.language) }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Profile - 星笺{% endblock %}
|
{% block title %}{{ 'nav.profiles'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h1>Profile</h1>
|
<h1>{{ 'nav.profiles'|t(user.language) }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-form">
|
<div class="inline-form">
|
||||||
<form method="post" action="/profiles" class="form-row">
|
<form method="post" action="/profiles" class="form-row">
|
||||||
<input type="text" name="nickname" placeholder="新 Profile 昵称" required>
|
<input type="text" name="nickname" placeholder="{{ 'profiles.new_ph'|t(user.language) }}" required>
|
||||||
<button type="submit" class="btn btn-primary">+ 新建</button>
|
<button type="submit" class="btn btn-primary">{{ 'profiles.new_btn'|t(user.language) }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% if profiles %}
|
{% if profiles %}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<div class="profile-card-header">
|
<div class="profile-card-header">
|
||||||
<h3 class="profile-card-name">
|
<h3 class="profile-card-name">
|
||||||
<span id="name-display-{{ p.id }}">{{ p.nickname }}</span>
|
<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>
|
</h3>
|
||||||
<form method="post" action="/profiles/{{ p.id }}/rename" class="rename-form" id="name-edit-{{ p.id }}" style="display:none">
|
<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>
|
<input type="text" name="nickname" value="{{ p.nickname }}" required>
|
||||||
@@ -29,61 +29,54 @@
|
|||||||
<div class="profile-card-stats">
|
<div class="profile-card-stats">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span class="stat-value">{{ e.total }}</span>
|
<span class="stat-value">{{ e.total }}</span>
|
||||||
<span class="stat-label">总计</span>
|
<span class="stat-label">{{ 'profiles.total'|t(user.language) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span class="stat-value badge-sent-text">{{ e.sent }}</span>
|
<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>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span class="stat-value badge-delivered-text">{{ e.delivered }}</span>
|
<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>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span class="stat-value badge-received-text">{{ e.received }}</span>
|
<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>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span class="stat-value">{{ e.countries }}</span>
|
<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>
|
</div>
|
||||||
<div class="profile-card-notes" style="padding:.5rem .75rem">
|
<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">
|
<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>
|
<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">保存</button>
|
<button type="submit" class="btn btn-sm" style="flex-shrink:0;margin-top:0">{{ 'profiles.save'|t(user.language) }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-card-footer">
|
<div class="profile-card-footer">
|
||||||
<span class="profile-card-date">创建于 {{ p.created_at.strftime('%Y-%m-%d') }}</span>
|
<span class="profile-card-date">{{ 'profiles.created_at'|t(user.language) }} {{ p.created_at.strftime('%Y-%m-%d') }}</span>
|
||||||
<div class="profile-card-actions">
|
<a href="/profiles/{{ p.id }}/postcards" class="btn btn-sm">{{ 'profiles.view'|t(user.language) }}</a>
|
||||||
<a href="/profiles/{{ p.id }}/postcards" class="btn btn-primary btn-sm">查看</a>
|
|
||||||
<form method="post" action="/profiles/{{ p.id }}/delete" class="inline">
|
<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>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state">
|
<p class="empty">{{ 'profiles.new_ph'|t(user.language) }}</p>
|
||||||
<div class="empty-icon">📮</div>
|
|
||||||
<p>还没有 Profile</p>
|
|
||||||
<p class="empty-sub">创建一个 Profile 来管理你的明信片吧</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function startRename(id) {
|
function startRename(id) {
|
||||||
document.getElementById('name-display-' + id).style.display = 'none';
|
document.getElementById('name-display-' + id).style.display = 'none';
|
||||||
var form = document.getElementById('name-edit-' + id);
|
document.getElementById('name-edit-' + id).style.display = '';
|
||||||
form.style.display = 'inline-flex';
|
document.getElementById('name-edit-' + id).querySelector('input').focus();
|
||||||
form.querySelector('input').focus();
|
|
||||||
}
|
}
|
||||||
function cancelRename(id) {
|
function cancelRename(id) {
|
||||||
var form = document.getElementById('name-edit-' + id);
|
|
||||||
form.style.display = 'none';
|
|
||||||
document.getElementById('name-display-' + id).style.display = '';
|
document.getElementById('name-display-' + id).style.display = '';
|
||||||
|
document.getElementById('name-edit-' + id).style.display = 'none';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block content %}
|
||||||
<div class="auth-form">
|
<div class="auth-form">
|
||||||
<h2>注册</h2>
|
<h2>{{ 'register.title'|t(user.language if user else 'zh') }}</h2>
|
||||||
<p style="color:var(--text-muted);font-size:.85rem;margin-bottom:1rem">邀请码由已有用户或管理员提供</p>
|
<p style="color:var(--text-muted);font-size:.85rem;margin-bottom:1rem">{{ 'register.hint'|t(user.language if user else 'zh') }}</p>
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="alert alert-error">{{ error }}</div>
|
<div class="alert alert-error">{{ error }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post" action="/register">
|
<form method="post" action="/register">
|
||||||
<label>邀请码</label>
|
<label>{{ 'register.invite_code'|t(user.language if user else 'zh') }}</label>
|
||||||
<input type="text" name="invite_code" required maxlength="8" placeholder="8位字母数字" style="text-transform:none" autofocus>
|
<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>用户名</label>
|
<label>{{ 'register.username'|t(user.language if user else 'zh') }}</label>
|
||||||
<input type="text" name="username" required maxlength="64">
|
<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">
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}用户设置 - 星笺{% endblock %}
|
{% block title %}{{ 'settings.title'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h1>用户设置</h1>
|
<h1>{{ 'settings.title'|t(user.language) }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tabs" id="tabs">
|
<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('account')">{{ 'settings.tab_account'|t(user.language) }}</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('showcase')">{{ 'settings.tab_showcase'|t(user.language) }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if success %}
|
{% if success %}
|
||||||
@@ -19,129 +19,94 @@
|
|||||||
|
|
||||||
<div id="tab-account" class="tab-panel" {% if active_tab == 'showcase' %}style="display:none"{% endif %}>
|
<div id="tab-account" class="tab-panel" {% if active_tab == 'showcase' %}style="display:none"{% endif %}>
|
||||||
<div class="form-card">
|
<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">
|
<form method="post" action="/settings/change-username">
|
||||||
<label>当前用户名</label>
|
<label>{{ 'settings.current_username'|t(user.language) }}</label>
|
||||||
<input type="text" value="{{ user.username }}" disabled>
|
<input type="text" value="{{ user.username }}" disabled>
|
||||||
<label>新用户名</label>
|
<label>{{ 'settings.new_username'|t(user.language) }}</label>
|
||||||
<input type="text" name="new_username" required maxlength="64" placeholder="输入新用户名">
|
<input type="text" name="new_username" required maxlength="64">
|
||||||
<div class="form-actions">
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-card" style="margin-top:1.5rem">
|
<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">
|
<form method="post" action="/settings/change-password">
|
||||||
<label>当前密码</label>
|
<label>{{ 'settings.current_password'|t(user.language) }}</label>
|
||||||
<input type="password" name="old_password" required placeholder="输入当前密码">
|
<input type="password" name="old_password" required>
|
||||||
<label>新密码</label>
|
<label>{{ 'settings.new_password'|t(user.language) }}</label>
|
||||||
<input type="password" name="new_password" required placeholder="输入新密码">
|
<input type="password" name="new_password" required>
|
||||||
<label>确认新密码</label>
|
<label>{{ 'settings.confirm_password'|t(user.language) }}</label>
|
||||||
<input type="password" name="confirm_password" required placeholder="再次输入新密码">
|
<input type="password" name="confirm_password" required>
|
||||||
<div class="form-actions">
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div id="tab-showcase" class="tab-panel" {% if active_tab != 'showcase' %}style="display:none"{% endif %}>
|
<div id="tab-showcase" class="tab-panel" {% if active_tab != 'showcase' %}style="display:none"{% endif %}>
|
||||||
<div class="form-card">
|
<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">
|
<form method="post" action="/showcase/manage">
|
||||||
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
<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">
|
<select name="showcase_mode" style="width:auto">
|
||||||
<option value="profile" {% if user.showcase_mode == 'profile' %}selected{% endif %}>按 Profile 分类</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 %}>全部合并</option>
|
<option value="flat" {% if user.showcase_mode == 'flat' %}selected{% endif %}>{{ 'settings.mode_flat'|t(user.language) }}</option>
|
||||||
</select>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<p style="margin-top:.75rem;font-size:.8rem;color:var(--text-muted)">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top:1.5rem">
|
|
||||||
<h2 style="font-size:1rem;margin-bottom:1rem">各 Profile 展示设置</h2>
|
|
||||||
{% if profiles %}
|
{% if profiles %}
|
||||||
|
<div style="margin-top:1.5rem">
|
||||||
|
<h2 style="font-size:1rem;margin-bottom:1rem">{{ 'settings.profiles_showcase'|t(user.language) }}</h2>
|
||||||
{% for p in profiles %}
|
{% for p in profiles %}
|
||||||
{% set sent_cards = [] %}
|
<div class="form-card" style="margin-bottom:.75rem">
|
||||||
{% set received_cards = [] %}
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem">
|
||||||
{% 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">
|
|
||||||
<strong>{{ p.nickname }}</strong>
|
<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>
|
</div>
|
||||||
<form method="post" action="/profiles/{{ p.id }}/showcase">
|
<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-wrap:wrap;gap:1rem;align-items:center">
|
<div style="display:flex;flex-direction:column;gap:.25rem">
|
||||||
<div style="display:flex;align-items:center;gap:.5rem">
|
<label style="margin:0;font-size:.78rem">{{ 'settings.enable_showcase'|t(user.language) }}</label>
|
||||||
<label style="margin:0;font-size:.85rem">开启展示</label>
|
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" name="showcase_enabled" value="1" {% if p.showcase_enabled %}checked{% endif %} onchange="this.form.submit()">
|
<input type="checkbox" name="showcase_enabled" value="1" {% if p.showcase_enabled %}checked{% endif %} onchange="this.form.submit()">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;align-items:center;gap:.5rem">
|
<div style="display:flex;flex-direction:column;gap:.25rem">
|
||||||
<label style="margin:0;font-size:.85rem">展示内容</label>
|
<label style="margin:0;font-size:.78rem">{{ 'settings.showcase_content'|t(user.language) }}</label>
|
||||||
<select name="showcase_visible" onchange="this.form.submit()" style="width:auto">
|
<select name="showcase_visible" onchange="this.form.submit()">
|
||||||
<option value="both" {% if p.showcase_visible == 'both' %}selected{% endif %}>发出 + 收到</option>
|
<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 %}>仅发出</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 %}>仅收到</option>
|
<option value="received" {% if p.showcase_visible == 'received' %}selected{% endif %}>{{ 'settings.content_received'|t(user.language) }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="/profiles/{{ p.id }}/showcase" class="btn btn-sm">{{ 'settings.save'|t(user.language) }}</a>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -151,5 +116,8 @@ 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 = '';
|
||||||
}
|
}
|
||||||
|
{% if active_tab == 'showcase' %}
|
||||||
|
document.querySelector('[onclick*="showcase"]').click();
|
||||||
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 nav %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h1>📮 {{ profile_user.username }} 的明信片</h1>
|
<h1>📮 {{ profile_user.username }} {{ 'showcase.title'|t(lang) }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% set has_content = (sent_all|length + received_all|length) > 0 %}
|
{% set has_content = (sent_all|length + received_all|length) > 0 %}
|
||||||
{% if not has_content %}
|
{% if not has_content %}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>暂无展示内容</p>
|
<p>{{ 'showcase.empty'|t(lang) }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<div class="tabs" id="tabs">
|
<div class="tabs" id="tabs">
|
||||||
<button class="tab active" onclick="switchTab('sent')">发出 ({{ sent_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')">收到 ({{ received_all|length }})</button>
|
<button class="tab" onclick="switchTab('received')">{{ 'showcase.tab_received'|t(lang) }} ({{ received_all|length }})</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tab-sent" class="tab-panel">
|
<div id="tab-sent" class="tab-panel">
|
||||||
@@ -113,76 +114,47 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div id="lightbox" class="lightbox" onclick="closeLightbox(event)">
|
<!-- Lightbox -->
|
||||||
<button class="lightbox-close" onclick="closeLightbox(event)">×</button>
|
<div id="lightbox" class="lightbox" onclick="closeLightbox()">
|
||||||
<button class="lightbox-prev" onclick="navLightbox(-1,event)">❮</button>
|
<button class="lightbox-close" onclick="closeLightbox()">✕</button>
|
||||||
<button class="lightbox-next" onclick="navLightbox(1,event)">❯</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()">
|
<div class="lightbox-content" onclick="event.stopPropagation()">
|
||||||
<img id="lightbox-img" src="" alt="">
|
<img id="lightbox-img" src="" alt="">
|
||||||
<div id="lightbox-caption" class="lightbox-caption"></div>
|
<div id="lightbox-number" class="lightbox-number"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<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) {
|
function switchTab(name) {
|
||||||
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
|
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
|
||||||
document.querySelectorAll('.tab-panel').forEach(function(p) { p.style.display = 'none'; });
|
document.querySelectorAll('.tab-panel').forEach(function(p) { p.style.display = 'none'; });
|
||||||
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>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}展示管理 - 星笺{% endblock %}
|
{% block title %}{{ 'showcase_manage.title'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div class="detail-title-row">
|
<div class="detail-title-row">
|
||||||
<a href="/profiles" class="btn btn-back">←</a>
|
<a href="/profiles" class="btn btn-back">←</a>
|
||||||
<h1>展示管理</h1>
|
<h1>{{ 'showcase_manage.title'|t(user.language) }}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-card">
|
<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">
|
<form method="post" action="/showcase/manage">
|
||||||
<label>展示页面模式</label>
|
<label>{{ 'settings.showcase_mode'|t(user.language) }}</label>
|
||||||
<select name="showcase_mode">
|
<select name="showcase_mode">
|
||||||
<option value="profile" {% if user.showcase_mode == 'profile' %}selected{% endif %}>按 Profile 分类</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 %}>全部合并</option>
|
<option value="flat" {% if user.showcase_mode == 'flat' %}selected{% endif %}>{{ 'settings.mode_flat'|t(user.language) }}</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="form-actions">
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<p class="form-hint" style="margin-top:.5rem;font-size:.8rem;color:var(--text-muted)">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top:1.5rem">
|
<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 %}
|
{% if profiles %}
|
||||||
{% for p in profiles %}
|
{% for p in profiles %}
|
||||||
{% set pc_list = profile_cards.get(p.id, []) %}
|
{% 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 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">
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem">
|
||||||
<strong>{{ p.nickname }}</strong>
|
<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>
|
</div>
|
||||||
<form method="post" action="/profiles/{{ p.id }}/showcase" style="display:flex;flex-wrap:wrap;gap:.75rem;align-items:flex-end">
|
<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">
|
<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">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" name="showcase_enabled" value="1" {% if p.showcase_enabled %}checked{% endif %} onchange="this.form.submit()">
|
<input type="checkbox" name="showcase_enabled" value="1" {% if p.showcase_enabled %}checked{% endif %} onchange="this.form.submit()">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;flex-direction:column;gap:.25rem">
|
<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()">
|
<select name="showcase_visible" onchange="this.form.submit()">
|
||||||
<option value="both" {% if p.showcase_visible == 'both' %}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 %}>仅发出</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 %}>仅收到</option>
|
<option value="received" {% if p.showcase_visible == 'received' %}selected{% endif %}>{{ 'showcase_manage.received_only'|t(user.language) }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% if p.showcase_enabled and pc_list %}
|
{% if p.showcase_enabled and pc_list %}
|
||||||
<div style="margin-top:.75rem;border-top:1px solid var(--border);padding-top:.75rem">
|
<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 %}
|
{% 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">
|
<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>
|
<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()">
|
<input type="checkbox" name="showcase_hidden" value="1" {% if pc.showcase_hidden %}checked{% endif %} onchange="this.form.submit()">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</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>
|
</form>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>暂无 Profile,请先创建</p>
|
<p>{{ 'profiles.new_ph'|t(user.language) }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user