feat: initial postcard manager - FastAPI + Jinja2 + SQLite
This commit is contained in:
34
app/templates/api_keys.html
Normal file
34
app/templates/api_keys.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}API Keys - Postcard Manager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="section-header">
|
||||
<h1>API Keys</h1>
|
||||
<form method="post" action="/api-keys">
|
||||
<button type="submit" class="btn btn-primary">+ 创建新 Key</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if keys %}
|
||||
<table>
|
||||
<thead><tr><th>Key</th><th>创建时间</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
{% for k in keys %}
|
||||
<tr>
|
||||
<td><code>{{ k.key }}</code></td>
|
||||
<td>{{ k.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>
|
||||
<form method="post" action="/api-keys/{{ k.id }}/delete" class="inline">
|
||||
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('确定删除该 Key?')">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="empty">还没有 API Key,点击上方按钮创建一个。</p>
|
||||
{% endif %}
|
||||
<section class="section" style="margin-top:2rem">
|
||||
<h2>使用说明</h2>
|
||||
<pre>curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8000/api/profiles</pre>
|
||||
</section>
|
||||
{% endblock %}
|
||||
24
app/templates/base.html
Normal file
24
app/templates/base.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Postcard Manager{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<a href="/dashboard" class="nav-brand">📮 Postcard Manager</a>
|
||||
<div class="nav-links">
|
||||
{% if user is defined and user %}
|
||||
<a href="/profiles">Profiles</a>
|
||||
<a href="/api-keys">API Keys</a>
|
||||
<a href="/logout" class="nav-logout">Logout</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
<main class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
27
app/templates/dashboard.html
Normal file
27
app/templates/dashboard.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard - Postcard Manager{% endblock %}
|
||||
{% block content %}
|
||||
<h1>👋 欢迎,{{ user.username }}</h1>
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Profiles</h2>
|
||||
</div>
|
||||
{% if profiles %}
|
||||
<div class="card-grid">
|
||||
{% for p in profiles %}
|
||||
<a href="/profiles/{{ p.id }}/postcards" class="card">
|
||||
<h3>{{ p.nickname }}</h3>
|
||||
<div class="stats">
|
||||
<span class="stat">📬 {{ stats[p.id].total }}</span>
|
||||
<span class="stat sent">寄出 {{ stats[p.id].sent }}</span>
|
||||
<span class="stat delivered">送达 {{ stats[p.id].delivered }}</span>
|
||||
<span class="stat received">收到 {{ stats[p.id].received }}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty">还没有 Profile,<a href="/profiles">去创建一个</a>。</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
18
app/templates/login.html
Normal file
18
app/templates/login.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}登录 - Postcard Manager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="auth-form">
|
||||
<h2>登录</h2>
|
||||
{% if error %}
|
||||
<div class="alert alert-error">{{ error }}</div>
|
||||
{% endif %}
|
||||
<form method="post" action="/login">
|
||||
<label>用户名</label>
|
||||
<input type="text" name="username" required autofocus>
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required>
|
||||
<button type="submit" class="btn btn-primary">登录</button>
|
||||
</form>
|
||||
<p class="auth-link">没有账号?<a href="/register">注册</a></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
55
app/templates/postcard_detail.html
Normal file
55
app/templates/postcard_detail.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}明信片详情 - Postcard Manager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="section-header">
|
||||
<h1>{{ postcard.card_number }}</h1>
|
||||
<div>
|
||||
<a href="/postcards/{{ postcard.id }}/edit" class="btn">编辑</a>
|
||||
<form method="post" action="/postcards/{{ postcard.id }}/delete" class="inline">
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('确定删除?')">删除</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge badge-{{ postcard.status }}">{{ {'sent':'已寄出','delivered':'已送达','received':'已收到'}.get(postcard.status, postcard.status) }}</span>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-images">
|
||||
<div class="image-block">
|
||||
<h3>正面</h3>
|
||||
{% if postcard.image_front %}
|
||||
<img src="{{ postcard.image_front }}" alt="正面" class="detail-img">
|
||||
{% endif %}
|
||||
<form method="post" action="/postcards/{{ postcard.id }}/image/front" enctype="multipart/form-data" class="inline upload-form">
|
||||
<input type="file" name="file" accept="image/*" required>
|
||||
<button type="submit" class="btn btn-sm">上传正面</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="image-block">
|
||||
<h3>反面</h3>
|
||||
{% if postcard.image_back %}
|
||||
<img src="{{ postcard.image_back }}" alt="反面" class="detail-img">
|
||||
{% endif %}
|
||||
<form method="post" action="/postcards/{{ postcard.id }}/image/back" enctype="multipart/form-data" class="inline upload-form">
|
||||
<input type="file" name="file" accept="image/*" required>
|
||||
<button type="submit" class="btn btn-sm">上传反面</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-meta">
|
||||
<dl>
|
||||
<dt>编号</dt><dd>{{ postcard.card_number }}</dd>
|
||||
<dt>状态</dt><dd>{{ {'sent':'已寄出','delivered':'已送达','received':'已收到'}.get(postcard.status, postcard.status) }}</dd>
|
||||
{% if postcard.status in ('sent','delivered') %}
|
||||
<dt>收件人</dt><dd>{{ postcard.recipient_name or '-' }}</dd>
|
||||
<dt>国家/地区</dt><dd>{{ postcard.country or '-' }}</dd>
|
||||
<dt>寄出时间</dt><dd>{{ postcard.send_time.strftime('%Y-%m-%d %H:%M') if postcard.send_time else '-' }}</dd>
|
||||
<dt>到达时间</dt><dd>{{ postcard.arrival_time.strftime('%Y-%m-%d %H:%M') if postcard.arrival_time else '-' }}</dd>
|
||||
{% else %}
|
||||
<dt>发件人</dt><dd>{{ postcard.sender_name or '-' }}</dd>
|
||||
<dt>收到时间</dt><dd>{{ postcard.receive_time.strftime('%Y-%m-%d %H:%M') if postcard.receive_time else '-' }}</dd>
|
||||
{% endif %}
|
||||
<dt>所属 Profile</dt><dd>{{ postcard.profile.nickname }}</dd>
|
||||
</dl>
|
||||
<a href="/profiles/{{ postcard.profile_id }}/postcards">← 返回列表</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
49
app/templates/postcard_form.html
Normal file
49
app/templates/postcard_form.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ '编辑' if postcard else '新建' }}明信片 - Postcard Manager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="section-header">
|
||||
<h1>{{ '编辑' if postcard else '新建' }}明信片</h1>
|
||||
</div>
|
||||
<form method="post" action="{{ '/postcards/%d/edit' % postcard.id if postcard else '/profiles/%d/postcards' % profile.id }}" class="form-card">
|
||||
<label>编号 *</label>
|
||||
<input type="text" name="card_number" value="{{ postcard.card_number if postcard else '' }}" required>
|
||||
|
||||
<label>状态 *</label>
|
||||
<select name="status" id="status-select" onchange="toggleFields()">
|
||||
<option value="sent" {% if postcard and postcard.status=='sent' %}selected{% endif %}>已寄出</option>
|
||||
<option value="delivered" {% if postcard and postcard.status=='delivered' %}selected{% endif %}>已送达</option>
|
||||
<option value="received" {% if postcard and postcard.status=='received' %}selected{% endif %}>已收到</option>
|
||||
</select>
|
||||
|
||||
<div id="fields-sent-delivered">
|
||||
<label>收件人</label>
|
||||
<input type="text" name="recipient_name" value="{{ postcard.recipient_name if postcard else '' }}">
|
||||
<label>国家/地区</label>
|
||||
<input type="text" name="country" value="{{ postcard.country if postcard else '' }}">
|
||||
<label>寄出时间</label>
|
||||
<input type="datetime-local" name="send_time" value="{{ postcard.send_time.strftime('%Y-%m-%dT%H:%M') if postcard and postcard.send_time else '' }}">
|
||||
<label>到达时间</label>
|
||||
<input type="datetime-local" name="arrival_time" value="{{ postcard.arrival_time.strftime('%Y-%m-%dT%H:%M') if postcard and postcard.arrival_time else '' }}">
|
||||
</div>
|
||||
|
||||
<div id="fields-received" style="display:none">
|
||||
<label>发件人</label>
|
||||
<input type="text" name="sender_name" value="{{ postcard.sender_name if postcard else '' }}">
|
||||
<label>收到时间</label>
|
||||
<input type="datetime-local" name="receive_time" value="{{ postcard.receive_time.strftime('%Y-%m-%dT%H:%M') if postcard and postcard.receive_time else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">保存</button>
|
||||
<a href="/profiles/{{ profile.id }}/postcards" class="btn">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
function toggleFields() {
|
||||
var s = document.getElementById('status-select').value;
|
||||
document.getElementById('fields-sent-delivered').style.display = (s === 'received') ? 'none' : '';
|
||||
document.getElementById('fields-received').style.display = (s === 'received') ? '' : 'none';
|
||||
}
|
||||
toggleFields();
|
||||
</script>
|
||||
{% endblock %}
|
||||
44
app/templates/postcards.html
Normal file
44
app/templates/postcards.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ profile.nickname }} - Postcard Manager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="section-header">
|
||||
<h1>{{ profile.nickname }} 的明信片</h1>
|
||||
<a href="/profiles/{{ profile.id }}/postcards/new" class="btn btn-primary">+ 新明信片</a>
|
||||
</div>
|
||||
<div class="filter-bar">
|
||||
<form method="get" class="form-row">
|
||||
<select name="status">
|
||||
<option value="">全部状态</option>
|
||||
<option value="sent" {% if status == 'sent' %}selected{% endif %}>已寄出</option>
|
||||
<option value="delivered" {% if status == 'delivered' %}selected{% endif %}>已送达</option>
|
||||
<option value="received" {% if status == 'received' %}selected{% endif %}>已收到</option>
|
||||
</select>
|
||||
<input type="text" name="country" placeholder="国家/地区" value="{{ country }}">
|
||||
<button type="submit" class="btn">筛选</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if postcards %}
|
||||
<div class="card-grid">
|
||||
{% for pc in postcards %}
|
||||
<a href="/postcards/{{ pc.id }}" class="postcard-card">
|
||||
{% if pc.image_front %}
|
||||
<img src="{{ pc.image_front }}" alt="正面" class="thumb">
|
||||
{% else %}
|
||||
<div class="thumb placeholder">📷</div>
|
||||
{% endif %}
|
||||
<div class="postcard-info">
|
||||
<span class="badge badge-{{ pc.status }}">{{ {'sent':'已寄出','delivered':'已送达','received':'已收到'}.get(pc.status, pc.status) }}</span>
|
||||
<strong>{{ pc.card_number }}</strong>
|
||||
{% if pc.status in ('sent','delivered') %}
|
||||
<span>→ {{ pc.recipient_name or '-' }} ({{ pc.country or '-' }})</span>
|
||||
{% else %}
|
||||
<span>← {{ pc.sender_name or '-' }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty">没有找到明信片。</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
33
app/templates/profiles.html
Normal file
33
app/templates/profiles.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Profiles - Postcard Manager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="section-header">
|
||||
<h1>Profiles</h1>
|
||||
</div>
|
||||
<div class="inline-form">
|
||||
<form method="post" action="/profiles" class="form-row">
|
||||
<input type="text" name="nickname" placeholder="新 Profile 昵称" required>
|
||||
<button type="submit" class="btn btn-primary">添加</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if profiles %}
|
||||
<table>
|
||||
<thead><tr><th>昵称</th><th>创建时间</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
{% for p in profiles %}
|
||||
<tr>
|
||||
<td><a href="/profiles/{{ p.id }}/postcards">{{ p.nickname }}</a></td>
|
||||
<td>{{ p.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>
|
||||
<form method="post" action="/profiles/{{ p.id }}/delete" class="inline">
|
||||
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('确定删除该 Profile 及其所有明信片?')">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="empty">还没有 Profile。</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
20
app/templates/register.html
Normal file
20
app/templates/register.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}注册 - Postcard Manager{% endblock %}
|
||||
{% block content %}
|
||||
<div class="auth-form">
|
||||
<h2>注册</h2>
|
||||
{% if error %}
|
||||
<div class="alert alert-error">{{ error }}</div>
|
||||
{% endif %}
|
||||
<form method="post" action="/register">
|
||||
<label>用户名</label>
|
||||
<input type="text" name="username" required autofocus>
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required>
|
||||
<label>确认密码</label>
|
||||
<input type="password" name="password2" required>
|
||||
<button type="submit" class="btn btn-primary">注册</button>
|
||||
</form>
|
||||
<p class="auth-link">已有账号?<a href="/login">登录</a></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user