307 lines
14 KiB
HTML
307 lines
14 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}{{ 'api_docs.title'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
|
||
{% block content %}
|
||
<style>
|
||
.doc-section { margin-bottom:2rem; }
|
||
.doc-section h2 { font-size:1.15rem; margin-bottom:.75rem; padding-bottom:.5rem; border-bottom:1px solid var(--border); }
|
||
.doc-section h3 { font-size:1rem; margin:1rem 0 .5rem; }
|
||
.doc-endpoint { background:var(--card-bg); border:1px solid var(--border); border-radius:8px; padding:1rem 1.25rem; margin-bottom:1rem; }
|
||
.doc-endpoint .ep-header { display:flex; align-items:center; gap:.6rem; margin-bottom:.5rem; flex-wrap:wrap; }
|
||
.doc-method { display:inline-block; padding:2px 8px; border-radius:4px; font-size:.75rem; font-weight:700; color:#fff; white-space:nowrap; }
|
||
.method-get { background:#22c55e; }
|
||
.method-post { background:#3b82f6; }
|
||
.method-put { background:#f59e0b; }
|
||
.method-delete { background:#ef4444; }
|
||
.doc-path { font-family:monospace; font-size:.9rem; color:var(--text); word-break:break-all; }
|
||
.doc-desc { color:var(--text-muted); font-size:.85rem; margin-bottom:.5rem; }
|
||
.doc-table { width:100%; border-collapse:collapse; font-size:.82rem; margin-top:.5rem; }
|
||
.doc-table th, .doc-table td { text-align:left; padding:4px 8px; border-bottom:1px solid var(--border); }
|
||
.doc-table th { color:var(--text-muted); font-weight:600; white-space:nowrap; }
|
||
.doc-table td:first-child { font-family:monospace; white-space:nowrap; }
|
||
.doc-note { color:var(--text-muted); font-size:.82rem; margin-top:.25rem; }
|
||
.doc-json { background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:.5rem .75rem; font-family:monospace; font-size:.8rem; margin-top:.5rem; overflow-x:auto; white-space:pre; }
|
||
.doc-auth-box { background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:1rem; margin:.75rem 0; }
|
||
.doc-auth-box code { background:var(--card-bg); padding:2px 6px; border-radius:3px; font-size:.82rem; }
|
||
.try-box { margin-top:.5rem; display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; }
|
||
.try-box input { flex:1; min-width:200px; font-family:monospace; font-size:.82rem; }
|
||
.try-box button { flex-shrink:0; }
|
||
.try-result { margin-top:.5rem; background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:.5rem .75rem; font-family:monospace; font-size:.78rem; max-height:300px; overflow:auto; white-space:pre-wrap; word-break:break-all; display:none; }
|
||
.try-result.show { display:block; }
|
||
.tag-required { color:var(--danger); font-size:.75rem; }
|
||
</style>
|
||
|
||
<div class="section-header">
|
||
<h1>{{ 'api_docs.title'|t(user.language) }}</h1>
|
||
</div>
|
||
|
||
<div class="doc-section">
|
||
<p style="color:var(--text-muted);font-size:.9rem">{{ 'api_docs.intro'|t(user.language) }}</p>
|
||
<p style="margin-top:.5rem"><a href="/settings?tab=apikeys" class="btn btn-sm">{{ 'api_docs.go_settings'|t(user.language) }}</a></p>
|
||
</div>
|
||
|
||
<div class="doc-section">
|
||
<h2>{{ 'api_docs.base_url'|t(user.language) }}</h2>
|
||
<div class="doc-json">{{ request.base_url }}api/v1</div>
|
||
</div>
|
||
|
||
<div class="doc-section">
|
||
<h2>{{ 'api_docs.auth'|t(user.language) }}</h2>
|
||
<p class="doc-desc">{{ 'api_docs.auth_desc'|t(user.language) }}</p>
|
||
<div class="doc-auth-box">
|
||
<code>X-API-Key: mv_your_api_key_here</code>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════ Profiles ═══════ -->
|
||
<div class="doc-section">
|
||
<h2>{{ 'api_docs.profile_ops'|t(user.language) }}</h2>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-get">GET</span>
|
||
<span class="doc-path">/profiles</span>
|
||
</div>
|
||
<div class="doc-desc">获取当前用户的所有 Profile 列表。</div>
|
||
<div class="doc-json">[
|
||
{"id": 1, "nickname": "Japan", "showcase_enabled": false, "showcase_visible": "both", "notes": "", "created_at": "2025-01-01T00:00:00"},
|
||
...
|
||
]</div>
|
||
</div>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-post">POST</span>
|
||
<span class="doc-path">/profiles</span>
|
||
</div>
|
||
<div class="doc-desc">创建新 Profile。</div>
|
||
<table class="doc-table">
|
||
<tr><th>{{ 'api_docs.body'|t(user.language) }}</th><th></th></tr>
|
||
<tr><td>nickname</td><td>string <span class="tag-required">{{ 'misc.required'|t(user.language) }}</span></td></tr>
|
||
</table>
|
||
<div class="doc-json">{"id": 2, "nickname": "Europe", ...}</div>
|
||
</div>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-get">GET</span>
|
||
<span class="doc-path">/profiles/{id}</span>
|
||
</div>
|
||
<div class="doc-desc">获取单个 Profile 详情。</div>
|
||
</div>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-put">PUT</span>
|
||
<span class="doc-path">/profiles/{id}</span>
|
||
</div>
|
||
<div class="doc-desc">更新 Profile。</div>
|
||
<table class="doc-table">
|
||
<tr><th>{{ 'api_docs.body'|t(user.language) }}</th><th></th></tr>
|
||
<tr><td>nickname</td><td>string(可选)</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-delete">DELETE</span>
|
||
<span class="doc-path">/profiles/{id}</span>
|
||
</div>
|
||
<div class="doc-desc">删除 Profile 及其下所有明信片。</div>
|
||
<div class="doc-json">{"ok": true}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════ Postcards ═══════ -->
|
||
<div class="doc-section">
|
||
<h2>{{ 'api_docs.postcard_ops'|t(user.language) }}</h2>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-get">GET</span>
|
||
<span class="doc-path">/profiles/{profile_id}/postcards</span>
|
||
</div>
|
||
<div class="doc-desc">获取指定 Profile 下的明信片列表。</div>
|
||
<table class="doc-table">
|
||
<tr><th>{{ 'api_docs.params'|t(user.language) }}</th><th></th></tr>
|
||
<tr><td>status</td><td>string(可选)— 按状态筛选:sent / delivered / received</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-post">POST</span>
|
||
<span class="doc-path">/profiles/{profile_id}/postcards</span>
|
||
</div>
|
||
<div class="doc-desc">创建新明信片。</div>
|
||
<table class="doc-table">
|
||
<tr><th>{{ 'api_docs.body'|t(user.language) }}</th><th></th></tr>
|
||
<tr><td>card_number</td><td>string <span class="tag-required">* {{ 'misc.required'|t(user.language) }}</span></td></tr>
|
||
<tr><td>status</td><td>string <span class="tag-required">* {{ 'misc.required'|t(user.language) }}</span> — sent / delivered / received</td></tr>
|
||
<tr><td>recipient_name</td><td>string(可选)</td></tr>
|
||
<tr><td>country</td><td>string(可选)— 两个大写字母,如 JP</td></tr>
|
||
<tr><td>send_time</td><td>string(可选)— 格式 YYYY-MM-DD 或 YYYY-MM-DDTHH:MM</td></tr>
|
||
<tr><td>arrival_time</td><td>string(可选)</td></tr>
|
||
<tr><td>sender_name</td><td>string(可选)</td></tr>
|
||
<tr><td>receive_time</td><td>string(可选)</td></tr>
|
||
<tr><td>notes</td><td>string(可选)</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-get">GET</span>
|
||
<span class="doc-path">/profiles/{profile_id}/postcards/{id}</span>
|
||
</div>
|
||
<div class="doc-desc">获取单张明信片详情。</div>
|
||
</div>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-put">PUT</span>
|
||
<span class="doc-path">/profiles/{profile_id}/postcards/{id}</span>
|
||
</div>
|
||
<div class="doc-desc">更新明信片。仅传入需要修改的字段。</div>
|
||
<table class="doc-table">
|
||
<tr><th>{{ 'api_docs.body'|t(user.language) }}</th><th></th></tr>
|
||
<tr><td>card_number</td><td>string(可选)</td></tr>
|
||
<tr><td>status</td><td>string(可选)</td></tr>
|
||
<tr><td>recipient_name</td><td>string(可选)</td></tr>
|
||
<tr><td>country</td><td>string(可选)</td></tr>
|
||
<tr><td>send_time</td><td>string(可选)</td></tr>
|
||
<tr><td>arrival_time</td><td>string(可选)</td></tr>
|
||
<tr><td>sender_name</td><td>string(可选)</td></tr>
|
||
<tr><td>receive_time</td><td>string(可选)</td></tr>
|
||
<tr><td>notes</td><td>string(可选)</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-delete">DELETE</span>
|
||
<span class="doc-path">/profiles/{profile_id}/postcards/{id}</span>
|
||
</div>
|
||
<div class="doc-desc">删除单张明信片。</div>
|
||
<div class="doc-json">{"ok": true}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════ Image Upload ═══════ -->
|
||
<div class="doc-section">
|
||
<h2>{{ 'api_docs.upload_op'|t(user.language) }}</h2>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-post">POST</span>
|
||
<span class="doc-path">/profiles/{profile_id}/postcards/{id}/upload</span>
|
||
</div>
|
||
<div class="doc-desc">上传明信片图片(正面或背面),使用 <code>multipart/form-data</code>。</div>
|
||
<table class="doc-table">
|
||
<tr><th>Form Field</th><th></th></tr>
|
||
<tr><td>file</td><td>File <span class="tag-required">* {{ 'misc.required'|t(user.language) }}</span> — 图片文件</td></tr>
|
||
<tr><td>side</td><td>string(可选)— front 或 back,默认 front</td></tr>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════ API Keys ═══════ -->
|
||
<div class="doc-section">
|
||
<h2>{{ 'api_docs.key_ops'|t(user.language) }}</h2>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-get">GET</span>
|
||
<span class="doc-path">/keys</span>
|
||
</div>
|
||
<div class="doc-desc">获取当前用户的所有 API Key。</div>
|
||
</div>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-post">POST</span>
|
||
<span class="doc-path">/keys</span>
|
||
</div>
|
||
<div class="doc-desc">创建新的 API Key。创建后完整 Key 仅在响应中显示一次。</div>
|
||
<table class="doc-table">
|
||
<tr><th>{{ 'api_docs.body'|t(user.language) }}</th><th></th></tr>
|
||
<tr><td>name</td><td>string(可选)— Key 的备注名称</td></tr>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-delete">DELETE</span>
|
||
<span class="doc-path">/keys/{id}</span>
|
||
</div>
|
||
<div class="doc-desc">删除指定 API Key。</div>
|
||
<div class="doc-json">{"ok": true}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════ Showcase ═══════ -->
|
||
<div class="doc-section">
|
||
<h2>{{ 'api_docs.showcase_ops'|t(user.language) }}</h2>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-get">GET</span>
|
||
<span class="doc-path">/showcase/{username}</span>
|
||
</div>
|
||
<div class="doc-desc">{{ 'api_docs.no_auth'|t(user.language) }} — 获取用户的公开展示数据。</div>
|
||
</div>
|
||
|
||
<div class="doc-endpoint">
|
||
<div class="ep-header">
|
||
<span class="doc-method method-get">GET</span>
|
||
<span class="doc-path">/showcase/{username}/profile/{id}</span>
|
||
</div>
|
||
<div class="doc-desc">{{ 'api_docs.no_auth'|t(user.language) }} — 获取单个 Profile 的展示数据。</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════ Try It ═══════ -->
|
||
<div class="doc-section">
|
||
<h2>{{ 'api_docs.try_it'|t(user.language) }}</h2>
|
||
<div class="try-box">
|
||
<input type="text" id="try-key" placeholder="{{ 'api_docs.api_key_ph'|t(user.language) }}">
|
||
<select id="try-method" style="width:auto;flex-shrink:0">
|
||
<option>GET</option><option>POST</option><option>PUT</option><option>DELETE</option>
|
||
</select>
|
||
<input type="text" id="try-url" placeholder="/profiles" style="flex:1;min-width:150px;font-family:monospace">
|
||
<button class="btn btn-primary btn-sm" onclick="tryApi()">{{ 'api_docs.send'|t(user.language) }}</button>
|
||
</div>
|
||
<textarea id="try-body" rows="3" placeholder="Request body (JSON, for POST/PUT)" style="width:100%;margin-top:.5rem;font-family:monospace;font-size:.82rem;resize:vertical;display:none"></textarea>
|
||
<div class="try-result" id="try-result"></div>
|
||
</div>
|
||
|
||
<script>
|
||
document.getElementById('try-method').addEventListener('change', function() {
|
||
document.getElementById('try-body').style.display = (this.value === 'POST' || this.value === 'PUT') ? '' : 'none';
|
||
});
|
||
|
||
async function tryApi() {
|
||
var key = document.getElementById('try-key').value.trim();
|
||
var method = document.getElementById('try-method').value;
|
||
var path = document.getElementById('try-url').value.trim();
|
||
var body = document.getElementById('try-body').value.trim();
|
||
var result = document.getElementById('try-result');
|
||
result.className = 'try-result show';
|
||
result.textContent = 'Loading...';
|
||
try {
|
||
var baseUrl = '{{ request.base_url }}api/v1';
|
||
var opts = { method: method, headers: {} };
|
||
if (key) opts.headers['X-API-Key'] = key;
|
||
if (body && (method === 'POST' || method === 'PUT')) {
|
||
opts.headers['Content-Type'] = 'application/json';
|
||
opts.body = body;
|
||
}
|
||
var resp = await fetch(baseUrl + path, opts);
|
||
var text = await resp.text();
|
||
try { text = JSON.stringify(JSON.parse(text), null, 2); } catch(e) {}
|
||
result.textContent = resp.status + ' ' + resp.statusText + '\n\n' + text;
|
||
} catch(e) {
|
||
result.textContent = 'Error: ' + e.message;
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %}
|